[Разбор] Полный разбор Games

Кликер с миссиями и настройками

Описание игры

Это динамичная игра-кликер, где задача игрока – как можно быстрее и точнее стрелять по движущимся целям на экране. Цели бывают двух типов: обычные зелёные и быстрые красные. При попадании очки увеличиваются, а при серии подряд – комбо накапливает бонусы. Игра разбита на уровни, которые повышают сложность (ускоряют и увеличивают число целей), а также содержит миссии на убийство определённого количества быстрых целей. Есть выбор длительности игры и уровня сложности. В игре есть звуковые эффекты выстрелов и взрывов, а интерфейс адаптивен под разные размеры экранов.


Возможности игры

Подробный разбор кода

1. Инициализация и настройка

Полный код игры:

game1.html:

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Кликер с миссиями и настройками</title>
<style>
 html, body {
  height: 100%;
  overflow-y: auto;
  margin: 0;
  padding: 0;
}
body {
  background: #121212;
  color: #eee;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  padding: 20px 10px 40px;
  user-select: none;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
}
h1 {
  margin-bottom: 10px;
  text-align: center;
  font-size: 1.6rem;
}
/* Стили для нового блока правил */
#rules {
  width: 90vw;
  max-width: 360px;
  background: #1a1a1a;
  border-radius: 12px;
  padding: 15px;
  margin-bottom: 20px;
  box-shadow: 0 0 15px #000;
  color: #ccc;
  box-sizing: border-box;
  font-size: 0.9rem;
  line-height: 1.4;
}
#rules h2 {
  color: #ffbb33;
  font-size: 1.1rem;
  margin: 0 0 10px;
  text-align: center;
}
#rules ul {
  padding-left: 20px;
  margin: 0;
}
#rules li {
  margin-bottom: 5px;
}

#gameArea {
  position: relative;
  width: 90vw;
  max-width: 360px;
  aspect-ratio: 3 / 5;
  max-height: 600px;
  min-height: 300px;
  background: #222;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 0 15px #000;
  margin: 0 auto 15px;
  box-sizing: border-box;
}
#gameArea > div, .projectile, .target, .explosion {
  position: absolute;
}
#startBtn, #fireBtn, #restartBtn {
  display: block;
  margin: 15px auto 10px;
  padding: 14px 40px;
  font-size: 24px;
  border-radius: 14px;
  border: none;
  color: white;
  cursor: pointer;
  user-select: none;
  min-width: 140px;
  max-width: 240px;
  width: 90vw;
  max-width: 360px;
  box-shadow: 0 0 12px;
}
#startBtn { background-color: #007bff; }
#fireBtn { background-color: #ff4a00; }
#fireBtn:disabled { background-color: #555; cursor: not-allowed; }
#restartBtn { background-color: #28a745; display: none; }

.projectile {
  width: 12px;
  height: 24px;
  background: linear-gradient(to top, #ff9e00, #ff3700);
  border-radius: 6px;
  bottom: 60px;
  z-index: 20;
  pointer-events: none;
}
.target {
  border-radius: 50%;
  box-shadow: 0 0 20px #00ff99;
  opacity: 0;
  transition: opacity 0.4s ease;
  z-index: 10;
  background-color: #00ff99 !important;
}
.target.fast {
  background-color: #ff5555 !important;
  box-shadow: 0 0 25px #ff5555;
}
.target.show { opacity: 1; }

.explosion {
  border-radius: 50%;
  pointer-events: none;
  animation: explodeAnim 0.4s forwards;
  background: radial-gradient(circle, #ffcc00, #ff6600);
}

@keyframes explodeAnim {
  0% { opacity: 1; transform: translate(0, 0) scale(1); }
  100% { opacity: 0; transform: translate(var(--tx), var(--ty)) scale(0); }
}

#score, #timer, #combo, #level, #mission, #difficultyDisplay {
  font-size: 1.4rem;
  margin: 5px 0;
  text-align: center;
  max-width: 360px;
  width: 90vw;
}
#difficultyDisplay { color: #ffbb33; margin-bottom: 10px; }
#result { color: #ffbb33; font-size: 1.6rem; margin-top: 20px; text-align: center; }
#settings {
  width: 90vw;
  max-width: 360px;
  background: #1a1a1a;
  border-radius: 12px;
  padding: 15px;
  margin-bottom: 20px;
  box-shadow: 0 0 15px #000;
  color: #ccc;
  box-sizing: border-box;
}
</style>
</head>
<body>
  <h1>Кликер с миссиями</h1>

  <!-- Твой новый блок с правилами -->
  <div id="rules">
    <h2>Как играть:</h2>
    <ul>
      <li>Цель: сбивать движущиеся мишени.</li>
      <li><b>Зелёные мишени:</b> обычные, дают 1 очко.</li>
      <li><b>Красные мишени:</b> быстрые, нужны для миссии.</li>
      <li><b>Комбо:</b> каждое попадание без промаха увеличивает комбо.</li>
      <li>Управление: кнопка «Выстрелить» или клавиша <b>Пробел</b>.</li>
      <li>Миссия: сбей нужное число красных мишеней для бонуса.</li>
    </ul>
  </div>

  <div id="settings">
    <label>Время (сек):</label>
    <select id="durationSelect"><option value="15">15</option><option value="30" selected>30</option><option value="60">60</option></select>
    <label>Сложность:</label>
    <select id="difficultySelect"><option value="easy">Лёгкий</option><option value="medium" selected>Средний</option><option value="hard">Сложный</option></select>
  </div>
  <div id="score">Очки: 0</div>
  <div id="timer">Время: 30</div>
  <div id="combo">Комбо: 0</div>
  <div id="level">Уровень: 1</div>
  <div id="difficultyDisplay">Сложность: Средний</div>
  <div id="mission">Миссия: Подготовка...</div>
  <div id="gameArea"></div>
  <button id="startBtn">Начать игру</button>
  <button id="fireBtn" disabled>Выстрелить!</button>
  <div id="result"></div>
  <button id="restartBtn">Сыграть заново</button>

<script>
// Весь твой JS без изменений
window.addEventListener('DOMContentLoaded', () => {
  const gameArea = document.getElementById('gameArea');
  const startBtn = document.getElementById('startBtn');
  const fireBtn = document.getElementById('fireBtn');
  const restartBtn = document.getElementById('restartBtn');
  const scoreDisplay = document.getElementById('score');
  const timerDisplay = document.getElementById('timer');
  const comboDisplay = document.getElementById('combo');
  const levelDisplay = document.getElementById('level');
  const difficultyDisplay = document.getElementById('difficultyDisplay');
  const missionDisplay = document.getElementById('mission');
  const resultDisplay = document.getElementById('result');
  const durationSelect = document.getElementById('durationSelect');
  const difficultySelect = document.getElementById('difficultySelect');

  let gameWidth, gameHeight, score, timeLeft, combo, maxCombo, gameRunning, targets, level, mission;
  const difficultySettings = {
    easy: {maxTargetsStart: 3, speedMultiplier: 0.7},
    medium: {maxTargetsStart: 5, speedMultiplier: 1},
    hard: {maxTargetsStart: 7, speedMultiplier: 1.5},
  };

  const ctx = new (window.AudioContext || window.webkitAudioContext)();
  function playSound(freq, duration=100) {
    if(ctx.state === 'suspended') ctx.resume();
    const osc = ctx.createOscillator();
    const gainNode = ctx.createGain();
    osc.type = 'triangle';
    osc.frequency.value = freq;
    gainNode.gain.setValueAtTime(0.1, ctx.currentTime);
    gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration/1000);
    osc.connect(gainNode);
    gainNode.connect(ctx.destination);
    osc.start();
    osc.stop(ctx.currentTime + duration/1000);
  }

  function updateGameAreaDimensions() {
    gameWidth = gameArea.clientWidth;
    gameHeight = gameArea.clientHeight;
  }

  function createTarget(type='normal') {
    const size = type==='fast' ? 35 : 50;
    const t = document.createElement('div');
    t.className = 'target ' + type;
    t.style.width = size + 'px';
    t.style.height = size + 'px';
    gameArea.appendChild(t);
    return {el: t, x: 0, y: 0, size, type, active: false, speedX: 0, speedY:0, disappearTimeout: null};
  }

  function createTargets() {
    updateGameAreaDimensions();
    if (targets) targets.forEach(t => { clearTimeout(t.disappearTimeout); t.el.remove(); });
    targets = [];
    const max = difficultySettings[difficultySelect.value].maxTargetsStart;
    for(let i=0; i < max; i++) targets.push(createTarget(i % 2 === 0 ? 'normal' : 'fast'));
  }

  function showTarget(target) {
    if(!gameRunning) return;
    target.x = Math.random() * (gameWidth - target.size - 10) + 5;
    target.y = Math.random() * (gameHeight - 200) + 50;
    target.el.style.left = target.x + 'px';
    target.el.style.top = target.y + 'px';
    target.el.style.opacity = "1";
    target.active = true;
    const baseSpeed = target.type === 'fast' ? 5 : 2;
    const mult = difficultySettings[difficultySelect.value].speedMultiplier;
    target.speedX = (Math.random() * baseSpeed + 1) * mult * (Math.random() > 0.5 ? 1 : -1);
    target.speedY = (Math.random() * baseSpeed + 1) * mult * (Math.random() > 0.5 ? 1 : -1);
    target.disappearTimeout = setTimeout(() => {
      target.active = false;
      target.el.style.opacity = "0";
      setTimeout(() => showTarget(target), 1000);
    }, 3000);
  }

  function createExplosion(x, y, size = 50) {
    for (let i = 0; i < 10; i++) {
        const spark = document.createElement('div');
        spark.className = 'explosion';
        const sSize = Math.random() * 8 + 4;
        spark.style.width = sSize + 'px';
        spark.style.height = sSize + 'px';
        spark.style.left = (x + size / 2) + 'px';
        spark.style.top = (y + size / 2) + 'px';
        spark.style.setProperty('--tx', (Math.random() - 0.5) * 150 + 'px');
        spark.style.setProperty('--ty', (Math.random() - 0.5) * 150 + 'px');
        gameArea.appendChild(spark);
        setTimeout(() => spark.remove(), 400);
    }
  }

  function shoot() {
    if(!gameRunning) return;
    const proj = document.createElement('div');
    proj.className = 'projectile';
    const leftPos = gameWidth / 2 - 6;
    proj.style.left = leftPos + 'px';
    proj.style.bottom = '60px';
    gameArea.appendChild(proj);
    playSound(600, 80);
    let posY = 60;
    function update() {
      posY += 15;
      proj.style.transform = `translateY(-${posY}px)`;
      const pRect = proj.getBoundingClientRect();
      for(const t of targets) {
        if(t.active) {
          const tRect = t.el.getBoundingClientRect();
          if(!(pRect.right < tRect.left || pRect.left > tRect.right || pRect.bottom < tRect.top || pRect.top > tRect.bottom)) {
            score++; combo++;
            if(t.type === 'fast') mission.fastTargetsHit++;
            scoreDisplay.textContent = `Очки: ${score}`;
            comboDisplay.textContent = `Комбо: ${combo}`;
            createExplosion(t.x, t.y, t.size);
            playSound(800 + combo * 20, 100);
            t.active = false; t.el.style.opacity = "0";
            clearTimeout(t.disappearTimeout);
            setTimeout(() => showTarget(t), 500);
            proj.remove(); return;
          }
        }
      }
      if(posY > gameHeight + 50) { proj.remove(); combo = 0; comboDisplay.textContent = `Комбо: 0`; }
      else requestAnimationFrame(update);
    }
    requestAnimationFrame(update);
  }

  function startGame() {
    // Добавляем активацию звука:
    if (ctx && ctx.state === 'suspended') ctx.resume();

    score = 0; combo = 0; level = 1; gameRunning = true;
    scoreDisplay.textContent = 'Очки: 0';
    resultDisplay.textContent = '';
    startBtn.style.display = 'none';
    restartBtn.style.display = 'none';
    fireBtn.disabled = false;
    updateGameAreaDimensions();
    createTargets();
    targets.forEach(t => showTarget(t));
    mission = { goal: 5, fastTargetsHit: 0 };
    missionDisplay.textContent = `Миссия: Убей ${mission.goal} красных`;
    timeLeft = parseInt(durationSelect.value);
    const timer = setInterval(() => {
      timeLeft--;
      timerDisplay.textContent = `Время: ${timeLeft}`;
      if(mission.fastTargetsHit >= mission.goal) missionDisplay.textContent = "Миссия выполнена! (+5 очков)";
      if(timeLeft <= 0) {
        clearInterval(timer);
        gameRunning = false;
        fireBtn.disabled = true;
        resultDisplay.textContent = `Конец! Счёт: ${score + (mission.fastTargetsHit >= mission.goal ? 5 : 0)}`;
        restartBtn.style.display = 'block';
      }
    }, 1000);
  }

  startBtn.addEventListener('click', startGame);
  restartBtn.addEventListener('click', startGame);
  fireBtn.addEventListener('click', shoot);
  window.addEventListener('keydown', (e) => {
    if (e.code === 'Space') {
      e.preventDefault();
      if(gameRunning) shoot(); else if(startBtn.style.display !== 'none') startGame();
    }
  });
  function loop() { if(gameRunning) {
    targets.forEach(t => {
      if(t.active) {
        t.x += t.speedX; t.y += t.speedY;
        if(t.x < 0 || t.x > gameWidth - t.size) t.speedX *= -1;
        if(t.y < 0 || t.y > gameHeight - 150) t.speedY *= -1;
        t.el.style.left = t.x + 'px'; t.el.style.top = t.y + 'px';
      }
    });
  } requestAnimationFrame(loop); }
  loop();
});
</script>
</body>
</html>

Платформер с уровнями сложности

Описание игры

Это классический 2D-платформер, в котором игрок управляет персонажем, преодолевая препятствия и избегая врагов на пути к цели. Игра требует точности движений и быстроты реакции. Игровой процесс разделён на уровни сложности, каждый из которых предлагает уникальные карты и временные ограничения. Особенностью игры является наличие анимированных эффектов (глаза персонажа, пыль при прыжках) и плавного управления, адаптированного как для клавиатуры, так и для мобильных устройств.


Возможности игры


Подробный разбор кода

1. Физика и движение

2. Рендеринг и анимация

3. Звуковое сопровождение

4. Управление и интерфейс

Полный код игры:

game2.html:

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Платформер с уровнями сложности</title>
<style>
  body {
    margin: 0; padding: 0;
    background: #222;
    font-family: Arial, sans-serif;
    color: #eee;
    text-align: center;
    user-select: none;
    overflow-x: hidden;
  }
  h1 {
    margin: 20px 0 10px;
    font-size: 1.5rem;
  }
  #rules {
    max-width: 600px;
    margin: 5px auto 10px;
    padding: 10px 15px;
    background: #333;
    border-radius: 12px;
    box-shadow: 0 0 15px #000;
    font-size: 14px;
    line-height: 1.4;
  }
  #gameControls {
    margin: 10px auto 15px;
    max-width: 700px;
  }
  select {
    padding: 8px 12px;
    font-size: 16px;
    border-radius: 8px;
    border: none;
    background: #444;
    color: #eee;
    cursor: pointer;
  }
  #infoPanel {
    margin-top: 6px;
    font-size: 18px;
    display: none;
    justify-content: center;
    gap: 20px;
  }
  canvas {
    background: #333;
    display: none;
    margin: 15px auto 5px;
    border: 3px solid #555;
    border-radius: 12px;
    max-width: 95vw;
    height: auto;
    touch-action: none;
  }
  #mobileControls {
    display: none;
    justify-content: center;
    gap: 30px;
    margin-bottom: 20px;
  }
  .btnControl {
    width: 70px;
    height: 70px;
    background: #555;
    border-radius: 50%;
    box-shadow: 0 0 15px #000;
    font-size: 24px;
    color: #eee;
    line-height: 70px;
    text-align: center;
    cursor: pointer;
    touch-action: none;
  }
  .btnControl.pressed {
    background: #ff7f50;
    box-shadow: 0 0 25px #ff7f50;
  }
  #startBtn {
    margin: 20px auto 30px;
    display: inline-block;
    padding: 14px 40px;
    font-size: 24px;
    font-weight: 700;
    border-radius: 14px;
    background: #ff4a00;
    color: #fff;
    cursor: pointer;
    box-shadow: 0 0 20px #ff4a00;
    border: none;
  }
  .p-dust {
    position: fixed;
    background: rgba(255,255,255,0.6);
    pointer-events: none;
    border-radius: 50%;
    z-index: 100;
  }
</style>
</head>
<body>
<h1>Платформер с уровнями сложности</h1>
<div id="rules">
  <strong>Правила:</strong>
  <ul style="text-align:left; padding-left: 20px; margin: 5px 0;">
    <li>Стрелки или кнопки для движения, Пробел или кнопка вверх для прыжка.</li>
    <li>Цель — зелёный квадрат. Враги — красные пульсирующие блоки.</li>
    <li>Касание врага или конец времени — перезапуск уровня.</li>
  </ul>
</div>
<div id="gameControls">
  Сложность: 
  <select id="difficultySelect">
    <option value="easy">Лёгкий</option>
    <option value="medium" selected>Средний</option>
    <option value="hard">Сложный</option>
  </select>
</div>
<div id="infoPanel">
  <div id="levelDisplay">Уровень: 1</div>
  <div id="score">Очки: 0</div>
  <div id="timer">Время: 0</div>
</div>
<canvas id="gameCanvas" width="700" height="400"></canvas>

<div id="mobileControls">
  <div id="btnLeft" class="btnControl">&#9668;</div>
  <div id="btnJump" class="btnControl">&#9650;</div>
  <div id="btnRight" class="btnControl">&#9658;</div>
</div>

<button id="startBtn">Начать игру</button>

<script>
  const canvas = document.getElementById('gameCanvas');
  const ctx = canvas.getContext('2d');
  const scoreElem = document.getElementById('score');
  const levelDisplay = document.getElementById('levelDisplay');
  const timerElem = document.getElementById('timer');
  const difficultySelect = document.getElementById('difficultySelect');
  const startBtn = document.getElementById('startBtn');
  const infoPanel = document.getElementById('infoPanel');
  const mobileControls = document.getElementById('mobileControls');

  const gravity = 0.5;
  let score = 0;
  let currentLevel = 0;
  let gameStarted = false;
  let timeLeft = 0;
  let lastTime = 0;

  // Звуковой контекст
  let audioCtx = null;
  function initAudio() {
    if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    if (audioCtx.state === 'suspended') audioCtx.resume();
  }

  function playSound(freq, type = 'sine', duration = 0.1) {
    if (!audioCtx) return;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type = type;
    osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
    gain.gain.setValueAtTime(0.1, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    osc.start();
    osc.stop(audioCtx.currentTime + duration);
  }

  const levelsData = {
    easy: [
      {
        platforms: [{x:0, y:350, width:700, height:50}, {x:150, y:280, width:120, height:15}, {x:350, y:230, width:120, height:15}, {x:550, y:180, width:120, height:15}],
        goal: {x: 620, y:130, width:30, height:30, color:'#0f0'},
        enemies: [{x:10, y:315, width:40, height:35, color:'#f00', speed:2, direction:1, range:{min:10,max:650}}],
        time: 90
      },
      {
        platforms: [{x:0, y:350, width:700, height:50}, {x:100, y:300, width:150, height:15}, {x:350, y:250, width:150, height:15}, {x:550, y:200, width:120, height:15}],
        goal: {x:620, y:170, width:30, height:30, color:'#0f0'},
        enemies: [{x:150, y:265, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:150,max:450}}],
        time: 85
      }
    ],
    medium: [
      {
        platforms: [{x:0, y:350, width:700, height:50}, {x:150, y:280, width:120, height:15}, {x:350, y:230, width:120, height:15}, {x:550, y:180, width:120, height:15}],
        goal: {x:620, y:130, width:30, height:30, color:'#0f0'},
        enemies: [{x:10, y:315, width:40, height:35, color:'#f00', speed:2, direction:1, range:{min:10,max:650}}, {x:400, y:195, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:400,max:650}}],
        time: 80
      },
      {
        platforms: [{x:0, y:350, width:700, height:50}, {x:100, y:300, width:150, height:15}, {x:350, y:250, width:150, height:15}, {x:550, y:200, width:150, height:15}],
        goal: {x:620, y:150, width:30, height:30, color:'#0f0'},
        enemies: [{x:150, y:265, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:150,max:400}}, {x:550, y:165, width:40, height:35, color:'#f00', speed:4, direction:1, range:{min:550,max:680}}],
        time: 75
      }
    ],
    hard: [
      {
        platforms: [{x:0, y:350, width:700, height:50}, {x:130, y:300, width:140, height:15}, {x:350, y:250, width:140, height:15}, {x:550, y:190, width:140, height:15}],
        goal: {x:620, y:130, width:30, height:30, color:'#0f0'},
        enemies: [{x:10, y:315, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:10,max:650}}, {x:420, y:215, width:40, height:35, color:'#f00', speed:4, direction:1, range:{min:420,max:650}}, {x:560, y:155, width:40, height:35, color:'#f00', speed:5, direction:1, range:{min:560,max:680}}],
        time: 60
      },
      {
        platforms: [{x:0, y:350, width:700, height:50}, {x:120, y:320, width:80, height:15}, {x:280, y:270, width:100, height:15}, {x:430, y:220, width:110, height:15}, {x:600, y:170, width:90, height:15}],
        goal: {x:650, y:120, width:30, height:30, color:'#0f0'},
        enemies: [{x:120, y:305, width:40, height:35, color:'#f00', speed:4, direction:1, range:{min:120,max:200}}, {x:280, y:255, width:40, height:35, color:'#f00', speed:5, direction:1, range:{min:280,max:380}}, {x:430, y:205, width:40, height:35, color:'#f00', speed:6, direction:1, range:{min:430,max:540}}, {x:600, y:155, width:40, height:35, color:'#f00', speed:7, direction:1, range:{min:600,max:690}}],
        time: 50
      }
    ]
  };

  const player = { x: 50, y: 0, width: 30, height: 50, color: '#ff9933', dy: 0, dx: 0, speed: 4, jumpStrength: 12, grounded: false };
  let platforms = [], goal = null, enemies = [];
  const keys = { left: false, right: false };

  function rectsCollide(r1, r2) {
    return !(r1.x > r2.x + r2.width || r1.x + r1.width < r2.x || r1.y > r2.y + r2.height || r1.y + r1.height < r2.y);
  }

  function update() {
    if(!gameStarted) return;

    // Расчет дельты времени для таймера
    let now = Date.now();
    let dt = (now - lastTime) / 1000;
    lastTime = now;
    timeLeft -= dt;
    timerElem.textContent = 'Время: ' + Math.ceil(Math.max(0, timeLeft));

    if(timeLeft <= 0) {
      playSound(150, 'sawtooth', 0.3);
      alert('Время вышло!');
      loadLevel(currentLevel);
      return;
    }

    player.dx = 0;
    if(keys.left) player.dx = -player.speed;
    if(keys.right) player.dx = player.speed;

    player.x += player.dx;
    player.dy += gravity;
    player.y += player.dy;

    player.grounded = false;

    // Коллизии с платформами
    for(let platform of platforms){
      if(player.x < platform.x + platform.width && player.x + player.width > platform.x &&
         player.y + player.height >= platform.y && player.y + player.height <= platform.y + platform.height && player.dy >= 0) {
        player.y = platform.y - player.height;
        player.dy = 0;
        player.grounded = true;
      }
    }

    // Границы канваса
    if(player.x < 0) player.x = 0;
    if(player.x + player.width > canvas.width) player.x = canvas.width - player.width;
    if(player.y + player.height > canvas.height){
      player.y = canvas.height - player.height;
      player.dy = 0;
      player.grounded = true;
    }

    // Враги
    for(let enemy of enemies){
      enemy.x += enemy.speed * enemy.direction;
      if(enemy.x > enemy.range.max || enemy.x < enemy.range.min) enemy.direction *= -1;
      if(rectsCollide(player, enemy)){
        playSound(100, 'square', 0.2);
        alert('Враг тебя поймал!');
        loadLevel(currentLevel);
        return;
      }
    }

    // Цель
    if(rectsCollide(player, goal)){
      playSound(800, 'sine', 0.4);
      score++;
      scoreElem.textContent = 'Очки: ' + score;
      currentLevel++;
      if(currentLevel >= levelsData[difficultySelect.value].length){
        alert('Победа! Все уровни пройдены!');
        currentLevel = 0;
        score = 0;
      }
      loadLevel(currentLevel);
    }
  }

 function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#999';
    for(let platform of platforms) ctx.fillRect(platform.x, platform.y, platform.width, platform.height);
    ctx.fillStyle = goal.color;
    ctx.fillRect(goal.x, goal.y, goal.width, goal.height);

    for(let enemy of enemies){
      ctx.fillStyle = enemy.color;
      let pulse = Math.sin(Date.now()/150) * 3;
      ctx.fillRect(enemy.x, enemy.y - pulse, enemy.width, enemy.height + pulse);
    }

    ctx.fillStyle = player.color;
    ctx.fillRect(player.x, player.y, player.width, player.height);
    ctx.fillStyle = 'white';
    let eyeOffset = keys.right ? 18 : (keys.left ? 2 : 10);
    ctx.fillRect(player.x + eyeOffset, player.y + 10, 5, 5);
    ctx.fillRect(player.x + eyeOffset + 7, player.y + 10, 5, 5);
  }

  function jump() {
    if(player.grounded) {
      playSound(400, 'sine', 0.1);
      player.dy = -player.jumpStrength;
      for(let i=0; i<5; i++) createDust(player.x + player.width/2, player.y + player.height);
    }
  }

  function createDust(x, y) {
    const d = document.createElement('div');
    d.className = 'p-dust';
    const size = Math.random() * 6 + 2;
    d.style.width = size + 'px';
    d.style.height = size + 'px';
    const rect = canvas.getBoundingClientRect();
    d.style.left = (rect.left + (x * (rect.width/canvas.width))) + 'px';
    d.style.top = (rect.top + (y * (rect.height/canvas.height))) + 'px';
    document.body.appendChild(d);

    let sx = (Math.random() - 0.5) * 4, sy = Math.random() * -2, op = 1;
    let anim = setInterval(() => {
      op -= 0.05; sy += 0.1;
      d.style.left = (parseFloat(d.style.left) + sx) + 'px';
      d.style.top = (parseFloat(d.style.top) + sy) + 'px';
      d.style.opacity = op;
      if(op <= 0) { clearInterval(anim); d.remove(); }
    }, 30);
  }

  startBtn.addEventListener('click', () => {
    initAudio();
    startBtn.style.display = 'none';
    canvas.style.display = 'block';
    infoPanel.style.display = 'flex';
    mobileControls.style.display = 'flex';
    loadLevel(0);
    lastTime = Date.now();
    gameLoop();
  });

  function gameLoop() {
    if(gameStarted) {
      update();
      draw();
      requestAnimationFrame(gameLoop);
    }
  }

  function loadLevel(index) {
    const lvl = levelsData[difficultySelect.value][index];
    platforms = [...lvl.platforms];
    goal = {...lvl.goal};
    enemies = lvl.enemies ? lvl.enemies.map(e => ({...e})) : [];
    player.x = 50; player.y = 0; player.dy = 0; player.grounded = false;
    timeLeft = lvl.time;
    lastTime = Date.now();
    levelDisplay.textContent = 'Уровень: ' + (index+1);
    gameStarted = true;
  }

  // Управление
  document.addEventListener('keydown', e => {
    if(e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
    if(e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
    if(e.code === 'ArrowUp' || e.code === 'Space' || e.code === 'KeyW') jump();
  });
  document.addEventListener('keyup', e => {
    if(e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
    if(e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
  });

  function bind(btn, key) {
    const start = (e) => { e.preventDefault(); if(key==='jump') jump(); else keys[key]=true; btn.classList.add('pressed'); };
    const end = (e) => { e.preventDefault(); if(key!=='jump') keys[key]=false; btn.classList.remove('pressed'); };
    btn.addEventListener('touchstart', start); btn.addEventListener('touchend', end);
    btn.addEventListener('mousedown', start); btn.addEventListener('mouseup', end);
  }
  bind(document.getElementById('btnLeft'), 'left');
  bind(document.getElementById('btnRight'), 'right');
  bind(document.getElementById('btnJump'), 'jump');

  window.addEventListener('resize', () => {
    const ratio = 700 / 400;
    let w = Math.min(window.innerWidth * 0.95, 700);
    canvas.style.width = w + 'px';
    canvas.style.height = (w / ratio) + 'px';
  });
  window.dispatchEvent(new Event('resize'));
</script>
</body>
</html>

Музыкальный реактор с секвенсером

Описание игры

Это интерактивный музыкальный секвенсер, где пользователь нажимает нотные кнопки, чтобы услышать ноты в режиме реального времени. Можно создавать и редактировать ритмические паттерны, активируя шаги в 16-таймном секвенсере для каждой ноты. Запускать последовательность звуков можно кнопкой "Старт" с возможностью регулировки темпа. Игра позволяет экспериментировать с музыкальными сочетаниями, создавая зацикленные мелодии в браузере с простой, но функциональной визуализацией и приятным дизайном.

Возможности игры

Подробный разбор кода

1. Инициализация и настройка аудио и элементов

Полный код игры:

game3.html:

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Музыкальный реактор с секвенсером</title>
<style>
  body {
    background: #121212;
    color: #eee;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 20px 10px 30px;
    user-select: none;
    min-height: 100vh;
    margin: 0;
  }
  h1 {
    margin-bottom: 10px;
    font-size: 1.8rem;
    text-align: center;
  }
  #rules {
    max-width: 480px;
    width: 100%;
    background: #222;
    padding: 15px;
    border-radius: 12px;
    box-shadow: 0 0 15px #000;
    margin-bottom: 20px;
    font-size: 1rem;
    line-height: 1.4;
    box-sizing: border-box;
  }
  #rules h2 {
    color: #ffbb33;
    margin-top: 0;
    font-weight: 600;
    text-align: center;
  }
  #v-canvas {
    width: 100%;
    max-width: 480px;
    background: #000;
    border-radius: 12px;
    margin-bottom: 15px;
    border: 2px solid #333;
    height: 100px;
  }
  .pad-container {
    display: grid;
    grid-template-columns: repeat(9, 1fr);
    gap: 10px;
    margin-bottom: 20px;
    max-width: 480px;
    width: 100%;
    box-sizing: border-box;
  }
  .pad {
    width: 100%;
    aspect-ratio: 1 / 1;
    max-width: 60px;
    background: #333;
    border-radius: 12px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 0.8rem;
    cursor: pointer;
    box-shadow: 0 0 10px #000;
    transition: background 0.2s, box-shadow 0.2s;
    touch-action: manipulation;
  }
  .pad.active {
    background: #ff6f61;
    box-shadow: 0 0 20px #ff6f61;
  }
  #sequencer {
    max-width: 480px;
    width: 100%;
    box-sizing: border-box;
    margin-bottom: 20px;
  }
  .sequencer-row {
    display: flex;
    justify-content: center;
    gap: 6px;
    margin-bottom: 8px;
  }
  .step {
    flex: 1;
    max-width: 30px;
    aspect-ratio: 1 / 1;
    background: #222;
    border-radius: 6px;
    cursor: pointer;
    box-shadow: 0 0 6px #000;
    transition: background 0.2s;
  }
  .step:nth-child(4n+1) { background: #2a2a2a; }
  .step.active {
    background: #ff6f61;
    box-shadow: 0 0 12px #ff6f61;
  }
  .step.playing {
    border: 2px solid #ffbb33;
    box-sizing: border-box;
  }
  #controls {
    margin-top: 10px;
    max-width: 480px;
    width: 100%;
    box-sizing: border-box;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 15px;
    justify-content: center;
  }
  #tempo {
    flex-grow: 1;
    min-width: 150px;
    max-width: 250px;
    cursor: pointer;
  }
  button {
    background-color: #ff4a00;
    border: none;
    border-radius: 14px;
    padding: 12px 25px;
    font-size: 1.1rem;
    color: white;
    font-weight: 600;
    cursor: pointer;
    box-shadow: 0 0 14px #ff4a00;
    flex: 1 1 120px;
  }
  button:disabled {
    background-color: #555;
    box-shadow: none;
    cursor: not-allowed;
  }
</style>
</head>
<body>
  <h1>Музыкальный реактор</h1>
  <div id="rules">
    <h2>Как играть:</h2>
    <ul>
      <li>Верхние кнопки — ноты в реальном времени.</li>
      <li>Сетка ниже — секвенсер. Отмечай шаги для авто-ритма.</li>
      <li>Нажми <b>"Старт"</b>, чтобы запустить бесконечный цикл.</li>
    </ul>
  </div>

  <canvas id="v-canvas"></canvas>

  <div class="pad-container" id="pads">
    <div class="pad" data-note="C4" tabindex="0">C4</div>
    <div class="pad" data-note="D4" tabindex="0">D4</div>
    <div class="pad" data-note="E4" tabindex="0">E4</div>
    <div class="pad" data-note="F4" tabindex="0">F4</div>
    <div class="pad" data-note="G4" tabindex="0">G4</div>
    <div class="pad" data-note="A4" tabindex="0">A4</div>
    <div class="pad" data-note="B4" tabindex="0">B4</div>
    <div class="pad" data-note="C5" tabindex="0">C5</div>
    <div class="pad" data-note="D5" tabindex="0">D5</div>
  </div>

  <div id="sequencer"></div>

  <div id="controls">
    <button id="startBtn">Старт</button>
    <button id="stopBtn" disabled>Стоп</button>
    <div style="width: 100%; text-align: center;">
      <label>Темп: <span id="tempoValue">120</span> BPM</label><br/>
      <input type="range" id="tempo" min="60" max="200" value="120" />
    </div>
  </div>

<script>
  const notesFreq = { C4: 261.63, D4: 293.66, E4: 329.63, F4: 349.23, G4: 392.00, A4: 440.00, B4: 493.88, C5: 523.25, D5: 587.33 };
  const noteKeys = Object.keys(notesFreq);

  let audioCtx = null;
  let analyser = null;
  let playing = false;
  let currentStep = 0;
  let nextStepTimeout = null;
  const stepsCount = 16;
  let sequencerData = noteKeys.map(() => Array(stepsCount).fill(false));

  const canvas = document.getElementById('v-canvas');
  const vCtx = canvas.getContext('2d');
  const startBtn = document.getElementById('startBtn');
  const stopBtn = document.getElementById('stopBtn');
  const tempoInput = document.getElementById('tempo');
  const tempoValue = document.getElementById('tempoValue');
  const sequencerDiv = document.getElementById('sequencer');

  function initAudio() {
    if (!audioCtx) {
      audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      analyser = audioCtx.createAnalyser();
      analyser.fftSize = 256;
      analyser.connect(audioCtx.destination);
      drawVisualizer();
    }
    if (audioCtx.state === 'suspended') audioCtx.resume();
  }

  function playNote(freq) {
    if (!audioCtx) return;
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.type = 'triangle';
    osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
    gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
    gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.4);
    osc.connect(gain);
    gain.connect(analyser);
    osc.start();
    osc.stop(audioCtx.currentTime + 0.4);
  }

  function drawVisualizer() {
    requestAnimationFrame(drawVisualizer);
    const bufferLength = analyser.frequencyBinCount;
    const dataArray = new Uint8Array(bufferLength);
    analyser.getByteFrequencyData(dataArray);
    vCtx.fillStyle = '#000';
    vCtx.fillRect(0, 0, canvas.width, canvas.height);
    const barWidth = (canvas.width / bufferLength) * 2.5;
    let x = 0;
    for (let i = 0; i < bufferLength; i++) {
        const h = dataArray[i] / 2;
        vCtx.fillStyle = `rgb(255, ${dataArray[i] + 100}, 50)`;
        vCtx.fillRect(x, canvas.height - h, barWidth, h);
        x += barWidth + 1;
    }
  }

  document.querySelectorAll('.pad').forEach(pad => {
    pad.addEventListener('click', () => {
      initAudio();
      playNote(notesFreq[pad.dataset.note]);
      pad.classList.add('active');
      setTimeout(() => pad.classList.remove('active'), 150);
    });
  });

  function createSequencer() {
    noteKeys.forEach((note, rowIndex) => {
      const row = document.createElement('div');
      row.className = 'sequencer-row';
      for (let i = 0; i < stepsCount; i++) {
        const stepBtn = document.createElement('div');
        stepBtn.className = 'step';
        stepBtn.onclick = () => {
          stepBtn.classList.toggle('active');
          sequencerData[rowIndex][i] = stepBtn.classList.contains('active');
        };
        row.appendChild(stepBtn);
      }
      sequencerDiv.appendChild(row);
    });
  }

  function playStep() {
    if (!playing) return;
    const interval = (60 / tempoInput.value) / 4 * 1000;

    document.querySelectorAll('.step.playing').forEach(el => el.classList.remove('playing'));
    noteKeys.forEach((note, rowIndex) => {
      const stepEl = sequencerDiv.children[rowIndex].children[currentStep];
      stepEl.classList.add('playing');
      if (sequencerData[rowIndex][currentStep]) playNote(notesFreq[note]);
    });

    currentStep = (currentStep + 1) % stepsCount;
    nextStepTimeout = setTimeout(playStep, interval);
  }

  startBtn.addEventListener('click', () => {
    initAudio();
    playing = true;
    startBtn.disabled = true;
    stopBtn.disabled = false;
    currentStep = 0;
    playStep();
  });

  stopBtn.addEventListener('click', () => {
    playing = false;
    clearTimeout(nextStepTimeout);
    startBtn.disabled = false;
    stopBtn.disabled = true;
    document.querySelectorAll('.step.playing').forEach(el => el.classList.remove('playing'));
  });

  tempoInput.addEventListener('input', () => tempoValue.textContent = tempoInput.value);

  createSequencer();
</script>
</body>
</html>

Мини-гольф с препятствиями

Описание игры

Это простая и увлекательная мини-гольф игра, где игрок управляет шариком на игровом поле с препятствиями. Задача – попасть шаром в лунку за минимальное количество ходов. Направление и сила удара задаются касанием и перемещением указателя мыши или пальца по экрану. После попадания выводится поздравление и игра сбрасывается. Есть кнопка для ручного сброса.


Возможности игры

Подробный разбор кода

1. Настройка канваса и адаптивность

Полный код игры:

game4.html:

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Мини-гольф с препятствиями</title>
<style>
  html, body {
    margin: 0; height: 100%;
    background: #121212;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    color: #eee;
    -webkit-tap-highlight-color: transparent;
    user-select: none;
    -webkit-user-select: none;
    -ms-user-select: none;
  }
  body {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    min-height: 100vh;
    padding: 10px 5px 15px;
  }
  #rules {
    background: #222;
    padding: 12px 16px;
    border-radius: 12px;
    max-width: 600px;
    width: 100%;
    box-sizing: border-box;
    margin-bottom: 10px;
    font-size: 1rem;
    line-height: 1.4;
    box-shadow: 0 0 15px rgba(100, 149, 237, 0.4);
  }
  #rules strong { color: #90caf9; }
  #info { font-size: 1.1rem; margin-bottom: 10px; text-align: center; font-weight: bold; }
  button {
    margin-bottom: 10px; padding: 12px 30px; border-radius: 12px; border: none;
    background-color: #ff6f61; color: white; font-weight: 600; cursor: pointer;
    box-shadow: 0 4px 0 #b34a41; transition: transform 0.1s;
    font-size: 1.1rem; max-width: 600px; width: 100%; box-sizing: border-box;
  }
  button:active { transform: translateY(3px); box-shadow: none; }
  canvas {
    background: #1a1a1a;
    border-radius: 12px;
    box-shadow: 0 0 30px #000;
    max-width: 600px;
    width: 100%;
    height: auto;
    touch-action: none;
  }
</style>
</head>
<body>

<div id="rules">
  <strong>Правила мини-гольфа:</strong>
  <ul style="margin: 5px 0; padding-left: 20px;">
    <li>Тяни от шарика, чтобы прицелиться и задать силу.</li>
    <li>Цвет линии подскажет мощность удара.</li>
    <li>Цель — лунка. Чем меньше ходов, тем лучше!</li>
  </ul>
</div>

<div id="info">Ходов: <span id="moves">0</span></div>
<button id="resetBtn">Сбросить игру</button>
<canvas id="game" width="600" height="400"></canvas>

<script>
(() => {
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const movesSpan = document.getElementById('moves');
  const resetBtn = document.getElementById('resetBtn');

  const BASE_WIDTH = 600;
  const BASE_HEIGHT = 400;

  const ball = { x: 100, y: 200, radius: 10, vx: 0, vy: 0, friction: 0.98 };
  const hole = { x: 520, y: 200, radius: 15 };
  const obstacles = [
    { x: 250, y: 100, width: 20, height: 200 },
    { x: 400, y: 50, width: 20, height: 150 }
  ];

  let moves = 0;
  let isDragging = false;
  let dragStart = null;
  let winAlerted = false;

  function draw() {
    ctx.clearRect(0, 0, BASE_WIDTH, BASE_HEIGHT);

    // Лунка
    ctx.beginPath();
    ctx.fillStyle = '#000';
    ctx.strokeStyle = '#90caf9';
    ctx.lineWidth = 3;
    ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();

    // Препятствия
    ctx.fillStyle = '#555a85';
    ctx.shadowBlur = 10;
    ctx.shadowColor = '#444';
    obstacles.forEach(obs => {
      ctx.fillRect(obs.x, obs.y, obs.width, obs.height);
      ctx.strokeStyle = '#666eb2';
      ctx.strokeRect(obs.x, obs.y, obs.width, obs.height);
    });
    ctx.shadowBlur = 0;

    // Линия прицела
    if (isDragging && dragStart) {
      const dx = ball.x - dragStart.x;
      const dy = ball.y - dragStart.y;
      const dist = Math.hypot(dx, dy);
      const intensity = Math.min(dist / 100, 1);

      ctx.beginPath();
      ctx.moveTo(ball.x, ball.y);
      ctx.lineTo(ball.x + dx, ball.y + dy);
      ctx.strokeStyle = `rgb(${intensity * 255}, ${255 - intensity * 255}, 255)`;
      ctx.lineWidth = 3;
      ctx.setLineDash([5, 5]);
      ctx.stroke();
      ctx.setLineDash([]);
    }

    // Шарик
    ctx.beginPath();
    ctx.fillStyle = '#ff6f61';
    ctx.shadowBlur = 15;
    ctx.shadowColor = '#ff6f61';
    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
    ctx.fill();
    ctx.shadowBlur = 0;
  }

  function update() {
    if (isDragging) return;

    ball.x += ball.vx;
    ball.y += ball.vy;
    ball.vx *= ball.friction;
    ball.vy *= ball.friction;

    // Стены
    if (ball.x < ball.radius || ball.x > BASE_WIDTH - ball.radius) {
      ball.vx *= -0.7;
      ball.x = ball.x < ball.radius ? ball.radius : BASE_WIDTH - ball.radius;
    }
    if (ball.y < ball.radius || ball.y > BASE_HEIGHT - ball.radius) {
      ball.vy *= -0.7;
      ball.y = ball.y < ball.radius ? ball.radius : BASE_HEIGHT - ball.radius;
    }

    // Столкновения с препятствиями
    obstacles.forEach(obs => {
      let cx = Math.max(obs.x, Math.min(ball.x, obs.x + obs.width));
      let cy = Math.max(obs.y, Math.min(ball.y, obs.y + obs.height));
      let dist = Math.hypot(ball.x - cx, ball.y - cy);

      if (dist < ball.radius) {
        if (Math.abs(ball.x - cx) > Math.abs(ball.y - cy)) ball.vx *= -0.7;
        else ball.vy *= -0.7;
        // Выталкивание
        let angle = Math.atan2(ball.y - cy, ball.x - cx);
        ball.x = cx + Math.cos(angle) * ball.radius;
        ball.y = cy + Math.sin(angle) * ball.radius;
      }
    });

    // Попадание в лунку
    let distToHole = Math.hypot(ball.x - hole.x, ball.y - hole.y);
    if (distToHole < hole.radius) {
        // Эффект засасывания
        ball.vx *= 0.5; ball.vy *= 0.5;
        if (distToHole < 5 && !winAlerted) {
          winAlerted = true;
          setTimeout(() => {
            alert(`В лунке! Ходов: ${moves}`);
            resetGame();
          }, 100);
        }
    }

    if (Math.abs(ball.vx) < 0.1) ball.vx = 0;
    if (Math.abs(ball.vy) < 0.1) ball.vy = 0;
  }

  function resetGame() {
    ball.x = 100; ball.y = 200; ball.vx = 0; ball.vy = 0;
    moves = 0; movesSpan.textContent = "0";
    winAlerted = false;
  }

  function getMousePos(e) {
    const rect = canvas.getBoundingClientRect();
    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
    const clientY = e.touches ? e.touches[0].clientY : e.clientY;
    return {
      x: (clientX - rect.left) * (BASE_WIDTH / rect.width),
      y: (clientY - rect.top) * (BASE_HEIGHT / rect.height)
    };
  }

  const startDrag = (e) => {
    const pos = getMousePos(e);
    if (Math.hypot(pos.x - ball.x, pos.y - ball.y) < ball.radius * 3) {
      isDragging = true;
      dragStart = pos;
    }
  };

  const doDrag = (e) => { if (isDragging) dragStart = getMousePos(e); };

  const endDrag = (e) => {
    if (isDragging) {
      const pos = getMousePos(e.changedTouches ? e.changedTouches[0] : e);
      ball.vx = (ball.x - pos.x) * 0.12;
      ball.vy = (ball.y - pos.y) * 0.12;
      isDragging = false;
      moves++;
      movesSpan.textContent = moves;
    }
  };

  canvas.addEventListener('mousedown', startDrag);
  window.addEventListener('mousemove', doDrag);
  window.addEventListener('mouseup', endDrag);

  canvas.addEventListener('touchstart', (e) => { e.preventDefault(); startDrag(e); }, {passive:false});
  window.addEventListener('touchmove', (e) => { if(isDragging) e.preventDefault(); doDrag(e); }, {passive:false});
  window.addEventListener('touchend', endDrag);

  resetBtn.onclick = resetGame;

  function loop() { update(); draw(); requestAnimationFrame(loop); }
  loop();
})();
</script>
</body>
</html>

Три в ряд – квадратная сетка

Описание игры

Это классическая игра «три в ряд», выполненная на квадратной сетке 8x8. Игрок меняет местами соседние шарики разного цвета, чтобы сформировать горизонтальные или вертикальные линии из трёх и более одинаковых по цвету фигур. Для разбивания специальных блоков нужно подбирать правильные ходы. Игра поддерживает два режима: по ходам и по времени, а также три уровня сложности, влияющие на палитру цветов и вероятность появления блоков.


Возможности игры

Подробный разбор кода

1. Инициализация и создание игрового поля

Полный код игры:

game5.html:

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Три в ряд </title>
<style>
  body {
    margin: 0; padding: 16px;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: #554e73;
    color: #fff;
    user-select: none;
    display: flex; flex-direction: column; align-items: center;
    min-height: 100vh;
  }
  #game-container {
    margin-top: 20px;
    width: 100%;
    max-width: 480px;
    padding: 0 10px;
    box-sizing: border-box;
  }
  #rules {
    background: rgba(255, 255, 255, 0.1);
    border-radius: 8px;
    padding: 10px;
    margin-bottom: 20px;
    font-size: 14px;
    line-height: 1.4;
  }
  #controls {
    margin-bottom: 12px;
    display: flex; justify-content: center; flex-wrap: wrap; gap: 10px;
  }
  button.level, button.mode {
    background: #444; border: none; border-radius: 8px; color: #eee;
    font-weight: 700; padding: 10px 20px; cursor: pointer;
    transition: background 0.3s; min-width: 90px; font-size: 16px;
  }
  button.level.active, button.mode.active { background: #90caf9; color: #121212; }

  #score, #counter, #combo {
    font-size: 20px; font-weight: 700; margin: 6px 0;
    text-shadow: 0 0 5px #000; text-align: center;
  }

  #game {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    grid-template-rows: repeat(8, 1fr);
    gap: 6px;
    background: #554e73;
    border-radius: 12px;
    touch-action: none;
    aspect-ratio: 1/1;
  }
  .cell {
    position: relative; width: 100%; aspect-ratio: 1/1;
    border-radius: 50%;
  }
  .ball {
    position: absolute; inset: 0; border-radius: 50%;
    background: radial-gradient(circle at 30% 35%, rgba(255,255,255,0.3), transparent 60%);
    box-shadow: inset 0 2px 6px rgba(255,255,255,0.5), inset 0 -5px 10px rgba(0,0,0,0.6);
    cursor: grab; transition: transform 0.2s, opacity 0.3s;
  }
  .ball.dragging { transform: scale(1.2); z-index: 10; cursor: grabbing; }

  .red { background-color: #8b2c2c; }
  .green { background-color: #317a31; }
  .blue { background-color: #1c5fa3; }
  .yellow { background-color: #bfae1e; }
  .orange { background-color: #b36b18; }
  .purple { background-color: #613a7f; }
  .cyan { background-color: #1b7d87; }
  .pink { background-color: #8c3360; }
  .lime { background-color: #8a8f2a; }

  .block .ball { background: #555 !important; border-radius: 8px; box-shadow: inset 0 0 12px #222 !important; cursor: not-allowed !important; }
  .block.hit1 .ball { background: #777 !important; border: 1px dashed #fff; }

  .removing { animation: fadeOutScale 0.4s forwards; }
  @keyframes fadeOutScale {
    0% { opacity: 1; transform: scale(1); }
    100% { opacity: 0; transform: scale(0.3); }
  }
</style>
</head>
<body>
<div id="game-container">
  <div id="rules">
    <strong>Правила:</strong> Собирай линии от 3 шаров. Блоки разбиваются рядом с комбинациями. Выбери режим и сложность!
  </div>
  <div id="controls">
    <button class="level active" data-level="easy">Лёгкий</button>
    <button class="level" data-level="medium">Средний</button>
    <button class="level" data-level="hard">Сложный</button>
    <button class="mode active" data-mode="moves">По ходам</button>
    <button class="mode" data-mode="time">По времени</button>
  </div>
  <div id="score">Счёт: 0</div>
  <div id="counter">Ходов осталось: 30</div>
  <div id="game"></div>
</div>

<script>
(() => {
  const levels = {
    easy:   { colors: 6, moves: 30, blockChance: 0.07, time: 60 },
    medium: { colors: 8, moves: 25, blockChance: 0.10, time: 45 },
    hard:   { colors: 9, moves: 20, blockChance: 0.14, time: 30 }
  };
  const colorPool = ['red', 'green', 'blue', 'yellow', 'orange', 'purple', 'cyan', 'pink', 'lime'];

  let currentLvl = 'easy';
  let mode = 'moves';
  let score = 0;
  let moves = 30;
  let timeLeft = 60;
  let timerId = null;
  let squares = [];
  let isProcessing = false;

  const game = document.getElementById('game');
  const scoreDisplay = document.getElementById('score');
  const counterDisplay = document.getElementById('counter');

  function init() {
    clearInterval(timerId);
    game.innerHTML = '';
    squares = [];
    score = 0;
    scoreDisplay.innerText = `Счёт: ${score}`;

    const cfg = levels[currentLvl];
    moves = cfg.moves;
    timeLeft = cfg.time;

    if(mode === 'moves') {
        counterDisplay.innerText = `Ходов осталось: ${moves}`;
    } else {
        counterDisplay.innerText = `Время осталось: ${timeLeft}s`;
        startTimer();
    }

    for(let i=0; i<64; i++) {
        const sq = document.createElement('div');
        sq.className = 'cell';
        const ball = document.createElement('div');
        ball.className = 'ball';
        if(Math.random() < cfg.blockChance) {
            sq.classList.add('block', 'hit2');
            sq.dataset.hits = 2;
        } else {
            ball.classList.add(colorPool[Math.floor(Math.random() * cfg.colors)]);
        }
        sq.appendChild(ball);
        game.appendChild(sq);
        squares.push(sq);
    }
    checkMatches(true);
  }

  function getBallColor(sq) {
    if(!sq || sq.classList.contains('block')) return null;
    return colorPool.find(c => sq.querySelector('.ball').classList.contains(c));
  }

  function checkMatches(silent = false) {
    let matched = new Set();
    for(let r=0; r<8; r++) {
        for(let c=0; c<6; c++) {
            let i = r*8+c;
            let c1 = getBallColor(squares[i]), c2 = getBallColor(squares[i+1]), c3 = getBallColor(squares[i+2]);
            if(c1 && c1 === c2 && c1 === c3) { matched.add(i); matched.add(i+1); matched.add(i+2); }
        }
    }
    for(let c=0; c<8; c++) {
        for(let r=0; r<6; r++) {
            let i = r*8+c;
            let c1 = getBallColor(squares[i]), c2 = getBallColor(squares[i+8]), c3 = getBallColor(squares[i+16]);
            if(c1 && c1 === c2 && c1 === c3) { matched.add(i); matched.add(i+8); matched.add(i+16); }
        }
    }

    if(matched.size > 0) {
        if(!silent) {
            isProcessing = true;
            matched.forEach(i => {
                squares[i].querySelector('.ball').classList.add('removing');
                hitBlocks(i);
            });
            score += matched.size * 10;
            scoreDisplay.innerText = `Счёт: ${score}`;
            setTimeout(() => {
                matched.forEach(i => {
                    const b = squares[i].querySelector('.ball');
                    b.className = 'ball ' + colorPool[Math.floor(Math.random() * levels[currentLvl].colors)];
                });
                isProcessing = false;
                checkMatches();
            }, 400);
        } else {
            matched.forEach(i => {
                const b = squares[i].querySelector('.ball');
                b.className = 'ball ' + colorPool[Math.floor(Math.random() * levels[currentLvl].colors)];
            });
            checkMatches(true);
        }
        return true;
    }
    return false;
  }

  function hitBlocks(idx) {
    [idx-1, idx+1, idx-8, idx+8].forEach(n => {
        if(squares[n] && squares[n].classList.contains('block')) {
            let h = parseInt(squares[n].dataset.hits);
            if(h > 1) { squares[n].dataset.hits = 1; squares[n].classList.replace('hit2', 'hit1'); }
            else { 
                squares[n].classList.remove('block', 'hit1'); 
                squares[n].querySelector('.ball').className = 'ball ' + colorPool[0];
            }
        }
    });
  }

  let dragSrc = null;
  game.addEventListener('mousedown', e => {
    const ball = e.target.closest('.ball');
    if(!ball || isProcessing || ball.parentElement.classList.contains('block')) return;
    dragSrc = ball.parentElement;
    ball.classList.add('dragging');
  });

  window.addEventListener('mouseup', e => {
    if(!dragSrc) return;
    dragSrc.querySelector('.ball').classList.remove('dragging');
    const target = e.target.closest('.cell');
    if(target && target !== dragSrc && !target.classList.contains('block')) {
        const i1 = squares.indexOf(dragSrc), i2 = squares.indexOf(target);
        if([1, -1, 8, -8].includes(i1 - i2)) {
            swap(dragSrc, target);
            if(!checkMatches()) { setTimeout(() => swap(dragSrc, target), 200); }
            else if(mode === 'moves') { 
                moves--; counterDisplay.innerText = `Ходов осталось: ${moves}`;
                if(moves <= 0) { alert('Финиш! Счёт: ' + score); init(); }
            }
        }
    }
    dragSrc = null;
  });

  function swap(s1, s2) {
    const b1 = s1.querySelector('.ball'), b2 = s2.querySelector('.ball');
    const c1 = b1.className, c2 = b2.className;
    b1.className = c2; b2.className = c1;
  }

  function startTimer() {
    timerId = setInterval(() => {
        timeLeft--;
        counterDisplay.innerText = `Время осталось: ${timeLeft}s`;
        if(timeLeft <= 0) { clearInterval(timerId); alert('Время вышло! Счёт: ' + score); init(); }
    }, 1000);
  }

  // Обработчики кнопок
  document.querySelectorAll('.level').forEach(b => b.onclick = () => {
    document.querySelector('.level.active').classList.remove('active');
    b.classList.add('active'); currentLvl = b.dataset.level; init();
  });

  document.querySelectorAll('.mode').forEach(b => b.onclick = () => {
    document.querySelector('.mode.active').classList.remove('active');
    b.classList.add('active'); mode = b.dataset.mode; init();
  });

  init();
})();
</script>
</body>
</html>

Тетрис

Описание игры

Тетрис – классическая и увлекательная игра-головоломка, где игрок управляет падающими фигурами разных форм и цветов на сетке размером 10×20 клеток. Цель – формировать полные горизонтальные линии из блоков, чтобы они исчезали, освобождая место для новых фигур и принося очки. Управление реализовано с помощью клавиатуры (стрелки и пробел) и сенсорных свайпов на мобильных устройствах. Игра предлагает выбор уровня сложности, отображает текущий счёт и уровень, а также обладает современным стильным интерфейсом с адаптивной графикой и плавной анимацией.


Возможности игры

Подробный разбор кода

1. Инициализация и подготовка поля

Полный код игры:

game6.html:

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<title>Тетрис</title>
<style>
  body {
    margin: 0;
    background: linear-gradient(135deg, #0d1117, #161b22);
    color: #e6e8ea;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    display: flex;
    flex-direction: column;
    align-items: center;
    min-height: 100vh;
    padding: 20px;
    user-select: none;
  }

  h1 {
    margin: 12px 0 24px;
    font-weight: 800;
    color: #58a6ff;
    text-shadow: 0 0 8px #58a6ff88;
    font-size: 2.6rem;
  }

  #container {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 24px;
    max-width: 480px;
    width: 100%;
  }

  #game {
    background: #0f161f;
    border-radius: 16px;
    box-shadow: 0 0 40px #2389d0cc inset, 0 8px 30px #145e96cc;
    touch-action: pan-y;
    outline: none;
    cursor: default;
    transition: box-shadow 0.3s ease;
    width: 240px;
    height: 480px;
    display: block;
    margin: 0 auto;
    image-rendering: pixelated;
  }

  #game:focus {
    box-shadow:
      0 0 48px #58a6ffcc inset,
      0 0 40px #58a6ff88,
      0 10px 38px #58a6ffcc;
  }

  #sidebar {
    width: 240px;
    display: flex;
    flex-direction: column;
    gap: 20px;
    background: linear-gradient(145deg, #1c2633, #121a26);
    padding: 24px;
    border-radius: 20px;
    box-shadow: 0 12px 36px rgba(10,30,60,0.7);
    user-select: none;
  }

  #score, #level {
    font-size: 1.4rem;
    font-weight: 800;
    padding: 14px 20px;
    background: linear-gradient(90deg, #204060, #1e3a66);
    border-radius: 18px;
    text-align: center;
    color: #a3c4ff;
    text-shadow: 0 0 10px #3f70cc;
    box-shadow: 0 2px 12px #2468c6aa;
  }

  #rules {
    max-width: 100%;
    background: #1a2332;
    padding: 18px 20px;
    border-radius: 16px;
    font-size: 0.9rem;
    line-height: 1.5;
    color: #bec9e2cc;
    box-shadow: inset 0 0 8px #0a1227;
    user-select: none;
    overflow-y: auto;
    max-height: 180px;
    font-weight: 500;
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  }

  #rules ul {
    padding-left: 20px;
    margin: 8px 0 0 0;
  }

  #rules li {
    margin-bottom: 10px;
  }

  button {
    padding: 14px 0;
    font-size: 1.2rem;
    font-weight: 900;
    border-radius: 20px;
    background: linear-gradient(135deg, #409cff, #0066ff);
    box-shadow:
      0 0 24px #3f88ffcc,
      inset 0 -3px 6px #0e5890cc;
    color: white;
    border: none;
    cursor: pointer;
    transition: background 0.3s ease, box-shadow 0.3s ease;
    user-select: none;
  }

  button:hover, button:focus {
    background: linear-gradient(135deg, #0053cc, #0041a8);
    box-shadow:
      0 0 30px #0053ccdd,
      inset 0 -3px 12px #002a65cc;
    outline: none;
  }

  label {
    font-size: 1.1rem;
    user-select: none;
    display: flex;
    align-items: center;
    gap: 14px;
    font-weight: 600;
    color: #aac2ffdd;
  }

  select {
    flex-grow: 1;
    background: #223250;
    color: #add0ff;
    border-radius: 14px;
    border: none;
    padding: 8px 14px;
    font-size: 1.05rem;
    cursor: pointer;
    box-shadow: inset 0 0 12px rgba(0,0,0,0.7);
    transition: background 0.3s, color 0.3s;
    font-weight: 600;
  }

  select:hover, select:focus {
    background: #2b3a66;
    color: #d6e4ff;
    outline: none;
  }

  @media (max-width: 650px) {
    #container {
      max-width: 100%;
      flex-direction: column;
      align-items: center;
    }
    #sidebar {
      width: 100%;
      padding: 18px;
      flex-direction: row;
      flex-wrap: wrap;
      justify-content: center;
      gap: 18px;
    }
    #rules {
      max-height: 120px;
      flex-basis: 100%;
      order: 3;
    }
    #score, #level {
      flex-grow: 1;
      font-size: 1.2rem;
    }
    button {
      flex-grow: 2;
      padding: 12px;
      font-size: 1.1rem;
      order: 2;
    }
    label {
      flex-grow: 2;
      order: 1;
    }
  }
</style>
</head>
<body>
  <h1>Тетрис</h1>
  <div id="container">
    <canvas id="game" width="240" height="480" aria-label="Игровое поле Тетриса" tabindex="0"></canvas>
    <div id="sidebar">
      <div id="score">Очки: 0</div>
      <div id="level">Уровень: 1</div>
      <label for="speedSelect">Сложность:
        <select id="speedSelect" aria-label="Выбор уровня сложности">
          <option value="800">Очень легко</option>
          <option value="500" selected>Средне</option>
          <option value="300">Сложно</option>
          <option value="150">Очень сложно</option>
        </select>
      </label>
      <button id="startBtn" aria-label="Начать игру">Старт</button>
      <button id="restartBtn" aria-label="Начать заново" disabled>Начать заново</button>
      <div id="rules" aria-label="Правила тетриса">
        <strong>Правила игры:</strong>
        <ul>
          <li>Используй стрелки влево &#8592; / вправо &#8594; для перемещения фигуры.</li>
          <li>Стрелка вниз &#8595; \u2014 ускорить падение фигуры.</li>
          <li>Пробел или стрелка вверх &#8593; \u2014 поворот фигуры.</li>
          <li>Собирай горизонтальные линии, чтобы они исчезали и приносили очки.</li>
          <li>Игра заканчивается, если фигуры достигают верха поля.</li>
        </ul>
      </div>
    </div>
  </div>

<script>
(() => {
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const scoreDisplay = document.getElementById('score');
  const levelDisplay = document.getElementById('level');
  const speedSelect = document.getElementById('speedSelect');
  const startBtn = document.getElementById('startBtn');
  const restartBtn = document.getElementById('restartBtn');

  const COLS = 10;
  const ROWS = 20;
  const BLOCK_SIZE = 24;

  let board = [];
  let score = 0;
  let level = 1;
  let dropInterval = 500;
  let dropCounter = 0;
  let lastTime = 0;
  let gameOver = false;
  let fastDrop = false;
  let fastDropTimeout;
  let running = false;

  let piece = null;
  let pieceX = 0;
  let pieceY = 0;

  const COLORS = [
    null,
    '#f44336',
    '#2196f3',
    '#ffeb3b',
    '#9c27b0',
    '#4caf50',
    '#ff9800',
    '#00bcd4',
  ];

  const SHAPES = [
    [],
    [ // I
      [0,0,0,0],
      [1,1,1,1],
      [0,0,0,0],
      [0,0,0,0],
    ],
    [ // J
      [2,0,0],
      [2,2,2],
      [0,0,0],
    ],
    [ // L
      [0,0,3],
      [3,3,3],
      [0,0,0],
    ],
    [ // O
      [4,4],
      [4,4],
    ],
    [ // S
      [0,5,5],
      [5,5,0],
      [0,0,0],
    ],
    [ // T
      [0,6,0],
      [6,6,6],
      [0,0,0],
    ],
    [ // Z
      [7,7,0],
      [0,7,7],
      [0,0,0],
    ],
  ];

  function fixDpi() {
    const dpi = window.devicePixelRatio || 1;
    canvas.width = COLS * BLOCK_SIZE * dpi;
    canvas.height = ROWS * BLOCK_SIZE * dpi;
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.scale(dpi, dpi);
  }

  fixDpi();
  window.addEventListener('resize', () => {
    fixDpi();
    draw();
  });

  function createBoard() {
    board = [];
    for(let y=0; y<ROWS; y++) {
      board[y] = new Array(COLS).fill(0);
    }
  }

  function drawSquare(x, y, colorIndex) {
    const xPx = Math.floor(x * BLOCK_SIZE);
    const yPx = Math.floor(y * BLOCK_SIZE);
    ctx.fillStyle = COLORS[colorIndex];
    ctx.fillRect(xPx+1, yPx+1, BLOCK_SIZE - 2, BLOCK_SIZE - 2);
    ctx.strokeStyle = "#101820";
    ctx.lineWidth = 2;
    ctx.strokeRect(xPx+1, yPx+1, BLOCK_SIZE - 2, BLOCK_SIZE - 2);
  }

  function drawBoard() {
    ctx.fillStyle = '#1f1f1f';
    ctx.fillRect(0, 0, COLS * BLOCK_SIZE, ROWS * BLOCK_SIZE);
    for(let y=0; y<ROWS; y++) {
      for(let x=0; x<COLS; x++) {
        if(board[y][x] !== 0) {
          drawSquare(x, y, board[y][x]);
        }
      }
    }
  }

  function drawPiece() {
    piece.shape.forEach((row, y) => {
      row.forEach((value, x) => {
        if(value !== 0) {
          drawSquare(pieceX + x, pieceY + y, value);
        }
      });
    });
  }

  function emptyTopRows(shape) {
    let emptyRows = 0;
    for(let y = 0; y < shape.length; y++) {
      if(shape[y].every(cell => cell === 0)) emptyRows++;
      else break;
    }
    return emptyRows;
  }

  function collide(offsetX=0, offsetY=0, shape = piece.shape) {
    for(let y=0; y<shape.length; y++) {
      for(let x=0; x<shape[y].length; x++) {
        if(shape[y][x] !== 0) {
          let newX = pieceX + x + offsetX;
          let newY = pieceY + y + offsetY;
          if(newX < 0 || newX >= COLS || newY >= ROWS) return true;
          if(newY >= 0 && board[newY][newX] !== 0) return true;
        }
      }
    }
    return false;
  }

  function mergePiece() {
    piece.shape.forEach((row, y) => {
      row.forEach((value, x) => {
        if(value !== 0 && pieceY + y >= 0) {
          board[pieceY + y][pieceX + x] = value;
        }
      });
    });
  }

  function rotate(matrix) {
    const N = matrix.length;
    const result = [];
    for(let y = 0; y < N; y++) {
      result[y] = [];
      for(let x = 0; x < N; x++) {
        result[y][x] = matrix[N - 1 - x][y];
      }
    }
    return result;
  }

  function rotatePiece() {
    const rotated = rotate(piece.shape);
    if(!collide(0, 0, rotated)) {
      piece.shape = rotated;
    } else {
      if(!collide(-1, 0, rotated)) {
        piece.shape = rotated;
        pieceX--;
      } else if(!collide(1, 0, rotated)) {
        piece.shape = rotated;
        pieceX++;
      }
    }
  }

  function clearLines() {
    let lines = 0;
    outer: for(let y = ROWS -1; y >= 0; y--) {
      for(let x = 0; x < COLS; x++) {
        if(board[y][x] === 0) continue outer;
      }
      board.splice(y, 1);
      board.unshift(new Array(COLS).fill(0));
      lines++;
      y++;
    }
    if(lines > 0) {
      score += lines * 10 * level;
      level = Math.min(10, Math.floor(score / 100) + 1);
      dropInterval = Math.floor(parseInt(speedSelect.value) / level);
      updateDisplay();
    }
  }

  function updateDisplay() {
    scoreDisplay.textContent = `Очки: ${score}`;
    levelDisplay.textContent = `Уровень: ${level}`;
  }

  function newPiece() {
    const id = Math.floor(Math.random() * (SHAPES.length - 1)) + 1;
    piece = {
      shape: SHAPES[id].map(row => row.slice()),
      id
    };
    pieceX = Math.floor(COLS / 2) - Math.ceil(piece.shape[0].length / 2);
    pieceY = -emptyTopRows(piece.shape);
    if(collide(0, 0)) {
      gameOver = true;
      stopGame();
    }
  }

  function resetGame() {
    score = 0;
    level = 1;
    dropInterval = Math.floor(parseInt(speedSelect.value));
    createBoard();
    newPiece();
    gameOver = false;
    updateDisplay();
  }

  function draw() {
    drawBoard();
    if(piece) drawPiece();
  }

  function update(time = 0) {
    if (!running) return;
    if(gameOver) {
      ctx.fillStyle = 'rgba(0,0,0,0.6)';
      ctx.fillRect(0, 0, COLS * BLOCK_SIZE, ROWS * BLOCK_SIZE);
      ctx.fillStyle = '#ff6f61';
      ctx.font = 'bold 36px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('Игра окончена', (COLS * BLOCK_SIZE) / 2, (ROWS * BLOCK_SIZE) / 2 - 10);
      ctx.font = '20px sans-serif';
      ctx.fillText(`Очки: ${score}`, (COLS * BLOCK_SIZE) / 2, (ROWS * BLOCK_SIZE) / 2 + 25);
      ctx.fillText('Нажми "Начать заново"', (COLS * BLOCK_SIZE) / 2, (ROWS * BLOCK_SIZE) / 2 + 55);
      stopGame();
      return;
    }
    if(!lastTime) lastTime = time;
    const delta = time - lastTime;
    dropCounter += delta;
    if(dropCounter > (fastDrop ? dropInterval / 5 : dropInterval)) {
      dropPiece();
      dropCounter = 0;
    }
    lastTime = time;
    draw();
    requestAnimationFrame(update);
  }

  function dropPiece() {
    if(!collide(0, 1)) {
      pieceY++;
    } else {
      mergePiece();
      clearLines();
      newPiece();
    }
  }

  function startGame() {
    if(running) return;
    dropInterval = Math.floor(parseInt(speedSelect.value));
    running = true;
    lastTime = 0;
    dropCounter = 0;
    score = 0;
    level = 1;
    updateDisplay();
    createBoard();
    newPiece();
    startBtn.disabled = true;
    restartBtn.disabled = false;
    update();
  }

  function stopGame() {
    running = false;
    startBtn.disabled = false;
    restartBtn.disabled = true;
  }

  window.addEventListener('keydown', e => {
    if (!running || gameOver) return;
    switch(e.code) {
      case 'ArrowLeft':
        if(!collide(-1, 0)) pieceX--;
        break;
      case 'ArrowRight':
        if(!collide(1, 0)) pieceX++;
        break;
      case 'ArrowDown':
        dropPiece();
        break;
      case 'ArrowUp':
      case 'Space':
        rotatePiece();
        break;
    }
  });

  speedSelect.addEventListener('change', () => {
    if (running) {
      dropInterval = Math.floor(parseInt(speedSelect.value) / level);
    }
  });

  startBtn.addEventListener('click', () => {
    resetGame();
    startGame();
  });

  restartBtn.addEventListener('click', () => {
    resetGame();
    startGame();
  });

  let touchStartX = 0;
  let touchStartY = 0;
  const swipeThreshold = 30;

  canvas.addEventListener('touchstart', e => {
    if(e.touches.length === 1) {
      touchStartX = e.touches[0].clientX;
      touchStartY = e.touches[0].clientY;
    }
  }, { passive: true });

  // Новый код для блокировки pull-to-refresh при свайпе вниз на канвасе
  let touchStartYForRefresh = 0;
  document.addEventListener('touchstart', e => {
    if (e.touches.length === 1) {
      touchStartYForRefresh = e.touches[0].clientY;
    }
  }, {passive: true});
  document.addEventListener('touchmove', e => {
    if (e.touches.length !== 1) return;

    const el = e.target;
    const currentY = e.touches[0].clientY;
    const diffY = currentY - touchStartYForRefresh;

    function isCanvasOrInside(element) {
      while (element) {
        if (element === canvas) return true;
        element = element.parentElement;
      }
      return false;
    }

    if (diffY > 0 && isCanvasOrInside(el)) {
      e.preventDefault();
    }
  }, {passive: false});


  canvas.addEventListener('touchmove', e => {
    if(e.touches.length === 1) {
      // Не вызываем e.preventDefault(), чтобы позволить скроллить страницу
    }
  }, { passive: false });

  canvas.addEventListener('touchend', e => {
    if (!running || gameOver) return;
    if(e.changedTouches.length === 1) {
      const dx = e.changedTouches[0].clientX - touchStartX;
      const dy = e.changedTouches[0].clientY - touchStartY;
      if(Math.abs(dx) > Math.abs(dy)) {
        if(dx > swipeThreshold) {
          if(!collide(1, 0)) pieceX++;
        } else if(dx < -swipeThreshold) {
          if(!collide(-1, 0)) pieceX--;
        }
      } else {
        if(dy > swipeThreshold) {
          fastDrop = true;
          dropPiece();
          clearTimeout(fastDropTimeout);
          fastDropTimeout = setTimeout(() => { fastDrop = false; }, 300);
        } else if(dy < -swipeThreshold) {
          rotatePiece();
        } else {
          if(Math.abs(dx) < 10 && Math.abs(dy) < 10) {
            rotatePiece();
          }
        }
      }
    }
  }, { passive: true });

  restartBtn.disabled = true;

  resetGame();
  draw();
})();
</script>
</body>
</html>

Пятнашки с уровнями и правилами

Описание игры

Это классическая логическая головоломка «Пятнашки», где игрок должен собрать все пронумерованные плитки в правильном порядке на квадратном поле с одной пустой клеткой. Размер поля и сложность можно выбрать из трёх вариантов: 3×3, 4×4 (стандартный) и 5×5. Перемещение плиток происходит путём тапов по соседним пустой клетке или с помощью стрелок клавиатуры. Игра ведёт подсчёт ходов и отображает поздравление при успешном решении головоломки. Интуитивно понятный интерфейс и управление адаптированы как для компьютеров, так и для мобильных устройств.


Возможности игры

Подробный разбор кода

1. Инициализация и создание игрового поля

Полный код игры:

game7.html:

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Пятнашки с уровнями и правилами</title>
<style>
  :root {
    --board-size: 4;
  }
  body {
    margin: 0; padding: 20px;
    background: #121212; color: #eee;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
    display: flex; flex-direction: column; align-items: center;
    user-select: none;
  }
  h1 {
    margin-bottom: 10px;
    font-weight: 600;
  }
  #rules {
    background-color: #1a1a1a;
    max-width: 600px;
    padding: 15px 20px;
    border-radius: 12px;
    box-shadow: 0 0 15px #000;
    font-size: 1rem;
    line-height: 1.4;
    color: #ccc;
    margin-bottom: 20px;
  }
  #rules h2 {
    color: #ffbb33;
    margin-top: 0;
    font-weight: 600;
    text-align: center;
  }
  #rules ul {
    padding-left: 20px;
    user-select: none;
  }
  label {
    margin-bottom: 10px;
    font-size: 1.1rem;
  }
  #levelSelect {
    margin-bottom: 15px;
    font-size: 1.1rem;
    padding: 5px 10px;
    border-radius: 6px;
  }
  #game {
    display: grid;
    gap: 8px;
    margin-bottom: 15px;
    user-select: none;
    grid-template-columns: repeat(var(--board-size), 80px);
    grid-template-rows: repeat(var(--board-size), 80px);
  }
  .tile {
    background: #ff4a00;
    border-radius: 10px;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 2rem;
    font-weight: 700;
    cursor: pointer;
    box-shadow: 0 0 12px #ff4a00;
    user-select: none;
    transition: background-color 0.2s;
    width: 80px;
    height: 80px;
  }
  .tile.empty {
    background: #222;
    box-shadow: none;
    cursor: default;
  }
  #info {
    margin-bottom: 15px;
    font-size: 1.2rem;
  }
  button {
    background-color: #ff4a00;
    border: none;
    border-radius: 14px;
    padding: 10px 30px;
    font-size: 1.1rem;
    color: white;
    font-weight: 600;
    cursor: pointer;
    box-shadow: 0 0 14px #ff4a00;
    user-select: none;
  }
  button:disabled {
    background-color: #555;
    box-shadow: none;
    cursor: not-allowed;
  }
  #result {
    margin-top: 15px;
    font-size: 1.3rem;
    min-height: 24px;
    text-align: center;
    color: #ffbb33;
    font-weight: 700;
  }
  @media (max-width: 480px) {
    #game {
      grid-template-columns: repeat(var(--board-size), 60px);
      grid-template-rows: repeat(var(--board-size), 60px);
    }
    .tile {
      width: 60px;
      height: 60px;
      font-size: 1.5rem;
      border-radius: 8px;
    }
    button {
      padding: 10px 20px;
      font-size: 1rem;
    }
  }
</style>
</head>
<body>
  <h1>Пятнашки с уровнями и правилами</h1>

  <section id="rules" aria-label="Правила игры в пятнашки">
    <h2>Правила игры</h2>
    <ul>
      <li>Выберите уровень сложности: <strong>3x3</strong>, <strong>4x4</strong> или <strong>5x5</strong>.</li>
      <li>Игра состоит из квадратного поля с пронумерованными плитками и одной пустой клеткой.</li>
      <li>Цель — собрать все плитки в порядке возрастания (слева направо, сверху вниз) пустая клетка внизу справа.</li>
      <li>Для перемещения тапай на плитку рядом с пустой клеткой или используй стрелки клавиатуры.</li>
      <li>Счётчик ходов считает каждое перемещение плитки.</li>
      <li>Если хочешь начать заново, нажми кнопку «Перезапустить».</li>
      <li>Игра адаптирована для смартфонов и компьютеров.</li>
    </ul>
  </section>

  <label for="levelSelect">Выберите уровень сложности:</label>
  <select id="levelSelect" aria-label="Выбор уровня пятнашек" name="levelSelect">
    <option value="3">Лёгкий (3x3)</option>
    <option value="4" selected>Средний (4x4)</option>
    <option value="5">Сложный (5x5)</option>
  </select>

  <div id="game" aria-label="Игровое поле пятнашки" role="application"></div>
  <div id="info" aria-live="polite">Ходы: 0</div>
  <button id="restart" aria-label="Перезапустить игру">Перезапустить</button>
  <div id="result" role="alert" aria-live="assertive"></div>

<script>
  const gameElem = document.getElementById('game');
  const infoElem = document.getElementById('info');
  const restartBtn = document.getElementById('restart');
  const resultElem = document.getElementById('result');
  const levelSelect = document.getElementById('levelSelect');

  let size = parseInt(levelSelect.value);
  let board = [];
  let emptyPos = {x: size - 1, y: size - 1};
  let moves = 0;

  function createBoard() {
    document.documentElement.style.setProperty('--board-size', size);
    gameElem.style.gridTemplateColumns = `repeat(${size}, 80px)`;
    gameElem.style.gridTemplateRows = `repeat(${size}, 80px)`;
    gameElem.innerHTML = '';
    const total = size * size;
    for(let i=0; i<total; i++) {
      const tile = document.createElement('div');
      tile.classList.add('tile');
      if(i === total - 1) {
        tile.classList.add('empty');
        tile.textContent = '';
      } else {
        tile.textContent = (i + 1).toString();
      }
      tile.dataset.index = i;
      tile.tabIndex = 0;
      tile.setAttribute('role', 'button');
      tile.setAttribute('aria-label', `Плитка ${tile.textContent || 'пустая'}`);
      tile.addEventListener('click', () => tryMove(i));
      tile.addEventListener('keydown', e => {
        if(e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          tryMove(i);
        }
      });
      gameElem.appendChild(tile);
    }
  }

  function shuffleBoard() {
    const total = size * size;
    board = [...Array(total).keys()];
    do {
      for(let i = board.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [board[i], board[j]] = [board[j], board[i]]; // вот исправленный обмен
      }
    } while(!isSolvable(board) || isSolved(board));

    emptyPos = {x: board.indexOf(0) % size, y: Math.floor(board.indexOf(0) / size)};
    moves = 0;
    updateDisplay();
    resultElem.textContent = '';
    infoElem.textContent = `Ходы: ${moves}`;
  }

  function updateDisplay() {
    const total = size * size;
    for(let i=0; i<total; i++) {
      const tile = gameElem.children[i];
      const val = board[i];
      if(val === 0){
        tile.classList.add('empty');
        tile.textContent = '';
        tile.setAttribute('aria-label', 'Пустая клетка');
      } else {
        tile.classList.remove('empty');
        tile.textContent = val.toString();
        tile.setAttribute('aria-label', `Плитка ${val}`);
      }
    }
    infoElem.textContent = `Ходы: ${moves}`;
  }

  function tryMove(index) {
    if(resultElem.textContent.length) return; // игра окончена
    const x = index % size;
    const y = Math.floor(index / size);
    const dx = Math.abs(x - emptyPos.x);
    const dy = Math.abs(y - emptyPos.y);
    if((dx === 1 && dy === 0) || (dy === 1 && dx === 0)) {
      const emptyIndex = emptyPos.y * size + emptyPos.x;
      [board[emptyIndex], board[index]] = [board[index], board[emptyIndex]];
      emptyPos = {x, y};
      moves++;
      updateDisplay();
      if(checkWin()){
        // блокируем клики после победы, можно, например, отключить все обработчики — здесь просто return
        // или можно сделать кнопку перезапуска более заметной
      }
    }
  }

  function checkWin() {
    const total = size * size;
    for(let i=0; i<total-1; i++) {
      if(board[i] !== i+1) return false;
    }
    resultElem.textContent = `Поздравляю! Ты собрал пятнашки ${size}x${size} за ${moves} ходов!`;
    return true;
  }

  function isSolved(arr) {
    for(let i=0; i<arr.length-1; i++) {
      if(arr[i] !== i+1) return false;
    }
    return true;
  }

  function isSolvable(array) {
    let invCount = 0;
    const total = size*size;
    for(let i=0; i<total-1; i++) {
      for(let j=i+1; j<total; j++) {
        if(array[i] && array[j] && array[i] > array[j]) invCount++;
      }
    }
    const emptyRow = Math.floor(array.indexOf(0) / size);

    if(size % 2 === 1) return invCount % 2 === 0;
    else {
      const blankRowFromBottom = size - 1 - emptyRow;
      if(blankRowFromBottom % 2 === 0) return invCount % 2 === 1;
      else return invCount % 2 === 0;
    }
  }

  window.addEventListener('keydown', (e) => {
    if(resultElem.textContent.length) return;
    let dx = 0, dy = 0;
    switch(e.key){
      case 'ArrowUp': dy = 1; break;
      case 'ArrowDown': dy = -1; break;
      case 'ArrowLeft': dx = 1; break;
      case 'ArrowRight': dx = -1; break;
      default: return;
    }
    e.preventDefault();
    const newX = emptyPos.x + dx;
    const newY = emptyPos.y + dy;
    if(newX >= 0 && newX < size && newY >= 0 && newY < size){
      const newIndex = newY * size + newX;
      const emptyIndex = emptyPos.y * size + emptyPos.x;
      [board[emptyIndex], board[newIndex]] = [board[newIndex], board[emptyIndex]];
      emptyPos = {x: newX, y: newY};
      moves++;
      updateDisplay();
      checkWin();
    }
  });

  restartBtn.addEventListener('click', () => {
    shuffleBoard();
  });

  levelSelect.addEventListener('change', () => {
    size = parseInt(levelSelect.value);
    emptyPos = {x: size - 1, y: size - 1};
    createBoard();
    shuffleBoard();
  });

  createBoard();
  shuffleBoard();
</script>
</body>
</html>

Главная страница «Сборник игр»

Описание страницы

Это удобная и стильная стартовая страница, служащая центральным каталогом для доступа к различным веб-играм из сборника. Она адаптирована под разные устройства и содержит краткий навигационный список всех доступных игр с лёгким переключением между тёмной и светлой темами интерфейса. Интерфейс интуитивно понятен и современен, благодаря аккуратному дизайну с комфортным размером элементов и плавным эффектам при наведении.


Возможности страницы

Подробный разбор кода

HTML

Полный код главной страницы:

index.htmll:

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>Сборник игр</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
  /* Используем системный шрифт без облачных подключений */
  body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
      Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
    background-color: #121212;
    color: #eee;
    display: flex;
    flex-direction: column;
    align-items: center;
    min-height: 100vh;
    padding: 40px 20px;
    transition: background-color 0.3s, color 0.3s;
  }

  .light {
    background-color: #f5f7fa;
    color: #222;
  }

  header {
    width: 100%;
    max-width: 480px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 40px;
  }

  h1 {
    font-weight: 600;
    font-size: 2.5rem;
    margin: 0;
  }

  button.toggle-theme {
    background: none;
    border: 2px solid currentColor;
    border-radius: 24px;
    padding: 6px 16px;
    font-weight: 600;
    font-size: 1rem;
    cursor: pointer;
    color: inherit;
    transition: background-color 0.3s, color 0.3s;
  }

  button.toggle-theme:hover {
    background-color: currentColor;
    color: #121212;
  }

  ul {
    list-style: none;
    padding: 0;
    width: 100%;
    max-width: 480px;
  }

  li {
    margin-bottom: 18px;
  }

  a {
    display: block;
    background-color: #1f1f1f;
    color: #90caf9;
    padding: 16px 24px;
    border-radius: 12px;
    text-decoration: none;
    font-weight: 600;
    font-size: 1.15rem;
    box-shadow: 0 3px 8px rgba(0,0,0,0.8);
    transition: background-color 0.3s, color 0.3s, box-shadow 0.3s, transform 0.3s;
    user-select: none;
  }

  .light a {
    background-color: #e3e9f6;
    color: #1a73e8;
    box-shadow: 0 3px 8px rgba(0,0,0,0.1);
  }

  a:hover {
    background-color: #90caf9;
    color: #121212;
    box-shadow: 0 8px 20px rgba(144,202,249,0.7);
    transform: translateY(-3px);
  }

  .light a:hover {
    background-color: #1a73e8;
    color: #f5f7fa;
    box-shadow: 0 8px 20px rgba(26,115,232,0.5);
  }

  @media (max-width: 480px) {
    h1 {
      font-size: 2rem;
    }
    a {
      font-size: 1.05rem;
      padding: 14px 20px;
    }
  }
</style>
</head>
<body>
  <header>
    <h1>Сборник игр</h1>
    <button class="toggle-theme" aria-label="Переключить тему">Светлая тема</button>
  </header>
  <ul>
    <li><a href="game1.html">Кликер на снаряды</a></li>
    <li><a href="game2.html">Платформер</a></li>
    <li><a href="game3.html">Музыкальный реактор</a></li>
    <li><a href="game4.html">Мини-гольф с физикой</a></li>
    <li><a href="game5.html">Три в ряд</a></li>
    <li><a href="game6.html">Тетрис</a></li>
    <li><a href="game7.html">Пятнашки</a></li>
  </ul>

<script>
  const button = document.querySelector('.toggle-theme');
  const body = document.body;

  function updateButtonText() {
    button.textContent = body.classList.contains('light') ? 'Тёмная тема' : 'Светлая тема';
  }

  button.addEventListener('click', () => {
    body.classList.toggle('light');
    updateButtonText();
  });

  updateButtonText();
</script>
</body>
</html>

Платформер с уровнями сложности

Описание игры

Игрок управляет рыжим персонажем на основе прямоугольника, который может ходить влево и вправо и прыгать по платформам, чтобы добраться до зелёного квадрата – цели уровня. В игре несколько уровней с возрастающей сложностью, на которых присутствуют враги и ограничения по времени. Попадание на врага или истечение времени приводит к перезапуску уровня. Есть возможность выбрать уровень сложности, который влияет на количество и поведение врагов, а также на продолжительность таймера.

В игре предусмотрена поддержка управления с клавиатуры (стрелки, пробел, WASD) и сенсорных кнопок на экране (для мобильных устройств). На экране отображается счёт, номер текущего уровня, таймер, а также выбор сложности и кнопка запуска.

Возможности игры

Подробный разбор кода

1. Глобальные переменные и элементы DOM

Полный код игры

game2.html:

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>Платформер с уровнями сложности</title>
<style>
  body {
    margin: 0; padding: 0;
    background: #222;
    font-family: Arial, sans-serif;
    color: #eee;
    text-align: center;
    user-select: none;
  }
  h1 {
    margin: 20px 0 10px;
    font-size: 1.5rem;
  }
  #rules {
    max-width: 600px;
    margin: 5px auto 10px;
    padding: 10px 15px;
    background: #333;
    border-radius: 12px;
    box-shadow: 0 0 15px #000;
    font-size: 14px;
    line-height: 1.4;
  }
  #gameControls {
    margin: 10px auto 15px;
    max-width: 700px;
  }
  select {
    padding: 8px 12px;
    font-size: 16px;
    border-radius: 8px;
    border: none;
    background: #444;
    color: #eee;
    cursor: pointer;
    user-select: none;
  }
  #infoPanel {
    margin-top: 6px;
    font-size: 18px;
    display: none;
    justify-content: center;
    gap: 20px;
  }
  #score, #levelDisplay, #timer {
    user-select: none;
    -webkit-user-select: none;
  }
  canvas {
    background: #333;
    display: none;
    margin: 15px auto 5px;
    border: 3px solid #555;
    border-radius: 12px;
    max-width: 95vw;
    height: auto;
  }
  #mobileControls {
    display: none;
    justify-content: center;
    gap: 30px;
    margin-bottom: 20px;
  }
  .btnControl {
    width: 75px;
    height: 75px;
    background: #555;
    border-radius: 50%;
    box-shadow: 0 0 15px #999;
    font-size: 30px;
    font-weight: bold;
    color: #eee;
    line-height: 75px;
    text-align: center;
    user-select: none;
    cursor: pointer;
    transition: background 0.2s, box-shadow 0.2s;
  }
  .btnControl.pressed {
    background: #ff7f50;
    box-shadow: 0 0 25px #ff7f50;
    color: #fff;
  }
  #startBtn {
    margin: 20px auto 30px;
    display: inline-block;
    padding: 14px 40px;
    font-size: 24px;
    font-weight: 700;
    border-radius: 14px;
    background: #ff4a00;
    color: #fff;
    cursor: pointer;
    box-shadow: 0 0 20px #ff4a00;
    user-select: none;
    border: none;
    transition: background 0.3s, box-shadow 0.3s;
  }
  #startBtn:hover {
    background: #ff6633;
    box-shadow: 0 0 30px #ff6633;
  }
</style>
</head>
<body>
<h1>Платформер с уровнями сложности</h1>
<div id="rules" aria-label="Правила игры">
  <strong>Правила:</strong>
  <ul style="text-align:left; padding-left: 20px; margin: 5px 0;">
    <li>Управление стрелками влево-вправо и пробел для прыжка (или сенсорными кнопками).</li>
    <li>Цель - дойти до зелёного квадрата (цель).</li>
    <li>Враги ходят по платформам. Столкновение с врагом - перезапуск уровня.</li>
    <li>Каждый уровень - таймер, по истечении которого нужно начинать заново.</li>
    <li>Выбирай сложность; чем выше, тем сложнее уровни и меньше времени.</li>
    <li>После прохождения всех уровней поздравление и сброс счёта.</li>
  </ul>
</div>
<div id="gameControls">
  Сложность: 
  <select id="difficultySelect" aria-label="Выбор сложности">
    <option value="easy">Лёгкий</option>
    <option value="medium" selected>Средний</option>
    <option value="hard">Сложный</option>
  </select>
</div>
<div id="infoPanel">
  <div id="levelDisplay">Уровень: 1</div>
  <div id="score">Очки: 0</div>
  <div id="timer">Время: 0</div>
</div>
<canvas id="gameCanvas" width="700" height="400" aria-label="Платформер"></canvas>

<div id="mobileControls" aria-label="Сенсорные кнопки управления">
  <div id="btnLeft" class="btnControl" role="button" tabindex="0" aria-pressed="false">&#9668;</div>
  <div id="btnJump" class="btnControl" role="button" tabindex="0" aria-pressed="false">&#9650;</div>
  <div id="btnRight" class="btnControl" role="button" tabindex="0" aria-pressed="false">&#9658;</div>
</div>

<button id="startBtn" type="button" aria-label="Начать игру">Начать игру</button>

<script>
  const canvas = document.getElementById('gameCanvas');
  const ctx = canvas.getContext('2d');
  const scoreElem = document.getElementById('score');
  const levelDisplay = document.getElementById('levelDisplay');
  const timerElem = document.getElementById('timer');
  const difficultySelect = document.getElementById('difficultySelect');

  const btnLeft = document.getElementById('btnLeft');
  const btnRight = document.getElementById('btnRight');
  const btnJump = document.getElementById('btnJump');

  const startBtn = document.getElementById('startBtn');
  const infoPanel = document.getElementById('infoPanel');
  const mobileControls = document.getElementById('mobileControls');

  const gravity = 0.5;
  let score = 0;
  let currentLevel = 0;
  let gameStarted = false;
  let timeLeft = 0;

  const levelsData = {
    easy: [
      {
        platforms: [
          {x:0, y:350, width:700, height:50},
          {x:150, y:280, width:120, height:15},
          {x:350, y:230, width:120, height:15},
          {x:550, y:180, width:120, height:15},
        ],
        goal: {x: 620, y:130, width:30, height:30, color:'#0f0'},
        enemies: [ {x:10, y:315, width:40, height:35, color:'#f00', speed:2, direction:1, range:{min:10,max:650}} ],
        time: 90
      },
      {
        platforms: [
          {x:0, y:350, width:700, height:50},
          {x:100, y:300, width:150, height:15},
          {x:350, y:250, width:150, height:15},
          {x:550, y:200, width:120, height:15},
        ],
        goal: {x:620, y:170, width:30, height:30, color:'#0f0'},
        enemies: [ {x:150, y:265, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:150,max:450}} ],
        time: 85
      }
    ],
    medium: [
      {
        platforms: [
          {x:0, y:350, width:700, height:50},
          {x:150, y:280, width:120, height:15},
          {x:350, y:230, width:120, height:15},
          {x:550, y:180, width:120, height:15},
        ],
        goal: {x:620, y:130, width:30, height:30, color:'#0f0'},
        enemies: [
          {x:10, y:315, width:40, height:35, color:'#f00', speed:2, direction:1, range:{min:10,max:650}},
          {x:400, y:195, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:400,max:650}}
        ],
        time: 80
      },
      {
        platforms: [
          {x:0, y:350, width:700, height:50},
          {x:100, y:300, width:150, height:15},
          {x:350, y:250, width:150, height:15},
          {x:550, y:200, width:150, height:15},
        ],
        goal: {x:620, y:150, width:30, height:30, color:'#0f0'},
        enemies: [
          {x:150, y:265, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:150,max:400}},
          {x:550, y:165, width:40, height:35, color:'#f00', speed:4, direction:1, range:{min:550,max:680}}
        ],
        time: 75
      }
    ],
    hard: [
      {
        platforms: [
          {x:0, y:350, width:700, height:50},
          {x:130, y:300, width:140, height:15},
          {x:350, y:250, width:140, height:15},
          {x:550, y:190, width:140, height:15},
        ],
        goal: {x:620, y:130, width:30, height:30, color:'#0f0'},
        enemies: [
          {x:10, y:315, width:40, height:35, color:'#f00', speed:3, direction:1, range:{min:10,max:650}},
          {x:420, y:215, width:40, height:35, color:'#f00', speed:4, direction:1, range:{min:420,max:650}},
          {x:560, y:155, width:40, height:35, color:'#f00', speed:5, direction:1, range:{min:560,max:680}}
        ],
        time: 60
      },
      {
        platforms: [
          {x:0, y:350, width:700, height:50},
          {x:120, y:320, width:80, height:15},
          {x:280, y:270, width:100, height:15},
          {x:430, y:220, width:110, height:15},
          {x:600, y:170, width:90, height:15}
        ],
        goal: {x:650, y:120, width:30, height:30, color:'#0f0'},
        enemies: [
          {x:120, y:305, width:40, height:35, color:'#f00', speed:4, direction:1, range:{min:120,max:200}},
          {x:280, y:255, width:40, height:35, color:'#f00', speed:5, direction:1, range:{min:280,max:380}},
          {x:430, y:205, width:40, height:35, color:'#f00', speed:6, direction:1, range:{min:430,max:540}},
          {x:600, y:155, width:40, height:35, color:'#f00', speed:7, direction:1, range:{min:600,max:690}}
        ],
        time: 50
      }
    ]
  };

  const player = {
    x: 50,
    y: 0,
    width: 30,
    height: 50,
    color: '#ff9933',
    dy: 0,
    dx: 0,
    speed: 4,
    jumpStrength: 12,
    grounded: false,
  };

  let platforms = [];
  let goal = null;
  let enemies = [];

  const keys = { left: false, right: false };

  function rectsCollide(r1, r2) {
    return !(r1.x > r2.x + r2.width || r1.x + r1.width < r2.x ||
             r1.y > r2.y + r2.height || r1.y + r1.height < r2.y);
  }

  function update() {
    if(!gameStarted) return;

    player.dx = 0;
    if(keys.left) player.dx = -player.speed;
    if(keys.right) player.dx = player.speed;

    player.x += player.dx;
    player.dy += gravity;
    player.y += player.dy;

    player.grounded = false;

    for(let platform of platforms){
      if(
        player.x < platform.x + platform.width &&
        player.x + player.width > platform.x &&
        player.y + player.height >= platform.y &&
        player.y + player.height <= platform.y + platform.height &&
        player.dy >= 0
      ) {
        player.y = platform.y - player.height;
        player.dy = 0;
        player.grounded = true;
      }
    }

    if(player.x < 0) player.x = 0;
    if(player.x + player.width > canvas.width) player.x = canvas.width - player.width;
    if(player.y + player.height > canvas.height){
      player.y = canvas.height - player.height;
      player.dy = 0;
      player.grounded = true;
    }

    for(let enemy of enemies){
      enemy.x += enemy.speed * enemy.direction;
      if(enemy.x > enemy.range.max || enemy.x < enemy.range.min){
        enemy.direction *= -1;
      }
    }

    for(let enemy of enemies){
      if(rectsCollide(player, enemy)){
        alert('Ты столкнулась с врагом! Уровень заново.');
        loadLevel(currentLevel);
        return;
      }
    }

    if(rectsCollide(player, goal)){
      score++;
      scoreElem.textContent = 'Очки: ' + score;
      currentLevel++;
      if(currentLevel >= levelsData[difficultySelect.value].length){
        alert('Отлично! Ты прошла все уровни со счётом: ' + score);
        currentLevel = 0;
        score = 0;
        scoreElem.textContent = 'Очки: 0';
      }
      loadLevel(currentLevel);
    }

    if(timeLeft > 0){
      timeLeft -= 1/60;
      timerElem.textContent = 'Время: ' + Math.ceil(timeLeft);
      if(timeLeft <= 0){
        alert('Время вышло! Уровень заново.');
        loadLevel(currentLevel);
      }
    }
  }

  function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = '#999';
    for(let platform of platforms){
      ctx.fillRect(platform.x, platform.y, platform.width, platform.height);
    }
    ctx.fillStyle = goal.color;
    ctx.fillRect(goal.x, goal.y, goal.width, goal.height);
    for(let enemy of enemies){
      ctx.fillStyle = enemy.color;
      ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
    }
    ctx.fillStyle = player.color;
    ctx.fillRect(player.x, player.y, player.width, player.height);
  }

  function jump() {
    if(player.grounded) {
      player.dy = -player.jumpStrength;
    }
  }

  function onKeyDown(e) {
    if(!gameStarted) return;
    if(e.repeat) return;
    if(e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = true;
    if(e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = true;
    if((e.code === 'ArrowUp' || e.code === 'Space' || e.code === 'KeyW')){
      jump();
    }
  }
  function onKeyUp(e) {
    if(!gameStarted) return;
    if(e.code === 'ArrowLeft' || e.code === 'KeyA') keys.left = false;
    if(e.code === 'ArrowRight' || e.code === 'KeyD') keys.right = false;
  }
  document.addEventListener('keydown', onKeyDown);
  document.addEventListener('keyup', onKeyUp);

  function bindControl(btn, key) {
    btn.addEventListener('touchstart', e => {
      e.preventDefault();
      if(!gameStarted) return;
      if(key === 'jump') jump();
      else keys[key] = true;
      btn.classList.add('pressed');
    }, {passive: false});
    btn.addEventListener('touchend', e => {
      e.preventDefault();
      if(!gameStarted) return;
      if(key !== 'jump') keys[key] = false;
      btn.classList.remove('pressed');
    }, {passive: false});

    btn.addEventListener('mousedown', e => {
      e.preventDefault();
      if(!gameStarted) return;
      if(key === 'jump') jump();
      else keys[key] = true;
      btn.classList.add('pressed');
    });
    btn.addEventListener('mouseup', e => {
      e.preventDefault();
      if(!gameStarted) return;
      if(key !== 'jump') keys[key] = false;
      btn.classList.remove('pressed');
    });
    btn.addEventListener('mouseleave', e => {
      e.preventDefault();
      if(!gameStarted) return;
      if(key !== 'jump') keys[key] = false;
      btn.classList.remove('pressed');
    });
  }

  bindControl(btnLeft, 'left');
  bindControl(btnRight, 'right');
  bindControl(btnJump, 'jump');

  function gameLoop() {
    update();
    draw();
    if(gameStarted) requestAnimationFrame(gameLoop);
  }

  function loadLevel(index) {
    const lvl = levelsData[difficultySelect.value][index];
    platforms = [...lvl.platforms];
    goal = {...lvl.goal};
    enemies = lvl.enemies ? lvl.enemies.map(e => ({...e})) : [];
    player.x = 50;
    player.y = 0;
    player.dy = 0;
    player.dx = 0;
    player.grounded = false;
    timeLeft = lvl.time;
    levelDisplay.textContent = 'Уровень: ' + (index+1) + ` (Сложность: ${difficultySelect.options[difficultySelect.selectedIndex].text})`;
    timerElem.textContent = 'Время: ' + timeLeft;
    gameStarted = true;
  }

  startBtn.addEventListener('click', () => {
    startBtn.style.display = 'none';
    difficultySelect.disabled = true;
    canvas.style.display = 'block';
    infoPanel.style.display = 'flex';
    mobileControls.style.display = 'flex';
    score = 0;
    scoreElem.textContent = 'Очки: 0';
    currentLevel = 0;
    loadLevel(currentLevel);
    gameLoop();
  });

  difficultySelect.addEventListener('change', () => {});

  function resizeCanvas() {
    const ratio = canvas.width / canvas.height;
    let width = window.innerWidth * 0.95;
    if(width > 700) width = 700;
    canvas.style.width = width + 'px';
    canvas.style.height = (width / ratio) + 'px';
  }
  window.addEventListener('resize', resizeCanvas);
  resizeCanvas();
</script>
</body>
</html>